Verifying signed git commits?

Git

Git Problem Overview


With newer versions of git it's possible to sign individual commits (in addition to tags) with a PGP key:

git commit -m "some message" -S

And you can show these signatures in the output of git log with the --show-signature option:

$ git log --show-signature
commit 93bd0a7529ef347f8dbca7efde43f7e99ab89515
gpg: Signature made Fri 28 Jun 2013 02:28:41 PM EDT using RSA key ID AC1964A8
gpg: Good signature from "Lars Kellogg-Stedman <[email protected]>"
Author: Lars Kellogg-Stedman <[email protected]>
Date:   Fri Jun 28 14:28:41 2013 -0400

    this is a test

But is there a way to programatically verify the signature on a given commit other than by grepping the output of git log? I'm looking for the commit equivalent of git tag -v -- something that will provide an exit code indicating whether or not there was a valid signature on a given commit.

Git Solutions


Solution 1 - Git

Just in case someone comes to this page through a search engine, like I did: New tools have been made available in the two years since the question was posted: There are now git commands for this task: git verify-commit and git verify-tag can be used to verify commits and tags, respectively.

Solution 2 - Git

Note: up to git 2.5, git verify-commit and git verify-tag only displayed a human readable message.
If you want to automate the check, git 2.6+ (Q3 2015) adds another output.

See commit e18443e, commit aeff29d, commit ca194d5, commit 434060e, commit 8e98e5f, commit a4cc18f, commit d66aeff (21 Jun 2015) by brian m. carlson (bk2204).
(Merged by Junio C Hamano -- gitster -- in commit ba12cb2, 03 Aug 2015)

> # verify-tag/verify-commit: add option to print raw gpg status information

> verify-tag/verify-commit by default displays human-readable output on standard error.
However, it can also be useful to get access to the raw gpg status information, which is machine-readable, allowing automated implementation of signing policy.
> > Add a --raw option to make verify-tag produce the gpg status information on standard error instead of the human-readable format.

Plus:

> verify-tag exits successfully if the signature is good but the key is untrusted. verify-commit exits unsuccessfully.
This divergence in behavior is unexpected and unwanted.
Since verify-tag existed earlier, add a failing test to have verify-commit share verify-tag's behavior.


git 2.9 (June 2016) update the git merge doc:

See commit 05a5869 (13 May 2016) by Keller Fuchs (``).
Helped-by: Junio C Hamano (gitster).
(Merged by Junio C Hamano -- gitster -- in commit be6ec17, 17 May 2016)

--verify-signatures:
--no-verify-signatures:

> Verify that the tip commit of the side branch being merged is signed with a valid key, i.e. a key that has a valid uid: in the default trust model, this means the signing key has been signed by a trusted key.
If the tip commit of the side branch is not signed with a valid key, the merge is aborted
.


Update Git 2.10 (Q3 2016)

See commit b624a3e (16 Aug 2016) by Linus Torvalds (torvalds).
(Merged by Junio C Hamano -- gitster -- in commit 83d9eb0, 19 Aug 2016)

> ## gpg-interface: prefer "long" key format output when verifying pgp signatures

> "git log --show-signature" and other commands that display the verification status of PGP signature now shows the longer key-id, as 32-bit key-id is so last century. > > Linus's original was rebased to apply to the maintenance track just in case binary distributors that are stuck in the past want to take it to their older codebase.


Git 2.11+ (Q4 2016) will even be more precise.

See commit 661a180 (12 Oct 2016) by Michael J Gruber (mjg).
(Merged by Junio C Hamano -- gitster -- in commit 56d268b, 26 Oct 2016)

> The GPG verification status shown in "%G?" pretty format specifier was not rich enough to differentiate a signature made by an expired key, a signature made by a revoked key, etc.
New output letters have been assigned to express them. > > According to gpg2's doc/DETAILS: > > > For each signature only one of the codes GOODSIG, BADSIG, EXPSIG, EXPKEYSIG, REVKEYSIG or ERRSIG will be emitted.

The git pretty-format documentation now include:

> - '%G?': show > - "G" for a good (valid) signature, > - "B" for a bad signature, > - "U" for a good signature with unknown validity, > - "X" for a good signature that has expired, > - "Y" for a good signature made by an expired key, > - "R" for a good signature made by a revoked key, > - "E" if the signature cannot be checked (e.g. missing key) and "N" for no signature


Git 2.12 (Q1 2017) "git tag" and "git verify-tag" learned to put GPG verification status in their "--format=<placeholders>" output format.

See commit 4fea72f, commit 02c5433, commit ff3c8c8 (17 Jan 2017) by Santiago Torres (SantiagoTorres).
See commit 07d347c, commit 2111aa7, commit 94240b9 (17 Jan 2017) by Lukas Puehringer (``).
(Merged by Junio C Hamano -- gitster -- in commit 237bdd9, 31 Jan 2017)

> Adding --format to git tag -v mutes the default output of the GPG verification and instead prints the formatted tag object.
This allows callers to cross-check the tagname from refs/tags with the tagname from the tag object header upon GPG verification.


Git 2.16 (Q1 2018) will allow the commit signature verification to be even more automated, with the merge.verifySignatures configuration variable.

See commit 7f8ca20, commit ca779e8 (10 Dec 2017) by Hans Jerry Illikainen (``).
(Merged by Junio C Hamano -- gitster -- in commit 0433d53, 28 Dec 2017)

> ## merge: add config option for verifySignatures

> git merge --verify-signatures can be used to verify that the tip commit of the branch being merged in is properly signed, but it's cumbersome to have to specify that every time. > > Add a configuration option that enables this behaviour by default, which can be overridden by --no-verify-signatures.

The git merge config man page now reads:

> merge.verifySignatures:

> If true, this is equivalent to the --verify-signatures command line option.


Git 2.19 (Q3 2018) is even more helpful, since "git verify-tag" and "git verify-commit" have been taught to use the exit status of underlying "gpg --verify" to signal bad or untrusted signature they found.

Note: with Git 2.19, gpg.format that can be set to "openpgp" or "x509", and gpg.<format>.program that is used to specify what program to use to deal with the format) to allow x.509 certs with CMS via "gpgsm" to be used instead of openpgp via "gnupg".

See commit 4e5dc9c (09 Aug 2018) by Junio C Hamano (gitster).
Helped-by: Vojtech Myslivec (VojtechMyslivec), brian m. carlson (bk2204), and Jeff King (peff).
(Merged by Junio C Hamano -- gitster -- in commit 4d34122, 20 Aug 2018)

> ## gpg-interface: propagate exit status from gpg back to the callers

> When gpg-interface API unified support for signature verification codepaths for signed tags and signed commits in mid 2015 at around v2.6.0-rc0~114, we accidentally loosened the GPG signature verification. > > Before that change, signed commits were verified by looking for "G"ood signature from GPG, while ignoring the exit status of "gpg --verify" process, while signed tags were verified by simply passing the exit status of "gpg --verify" through.
> > The unified code we currently have ignores the exit status of "gpg --verify" and returns successful verification when the signature matches an unexpired key regardless of the trust placed on the key (i.e. in addition to "G"ood ones, we accept "U"ntrusted ones). > > Make these commands signal failure with their exit status when underlying "gpg --verify" (or the custom command specified by "gpg.program" configuration variable) does so.
This essentially changes their behaviour in a backward incompatible way to reject signatures that have been made with untrusted keys even if they correctly verify, as that is how "gpg --verify" behaves. > > Note that the code still overrides a zero exit status obtained from "gpg" (or gpg.program) if the output does not say the signature is good or computes correctly but made with untrusted keys, to catch a poorly written wrapper around "gpg" the user may give us. > > We could exclude "U"ntrusted support from this fallback code, but that would be making two backward incompatible changes in a single commit, so let's avoid that for now.
A follow-up change could do so if desired.


> the key is needed to be trusted/signed before do any encryption

On the trust side, there is progress:
With Git 2.26 (Q1 2020), gpg.minTrustLevel configuration variable has been introduced to tell various signature verification codepaths the required minimum trust level.

See commit 54887b4 (27 Dec 2019) by Hans Jerry Illikainen (illikainen).
(Merged by Junio C Hamano -- gitster -- in commit 11ad30b, 30 Jan 2020)

> ## gpg-interface: add minTrustLevel as a configuration option
> Signed-off-by: Hans Jerry Illikainen

> Previously, signature verification for merge and pull operations checked if the key had a trust-level of either TRUST_NEVER or TRUST_UNDEFINED in verify_merge_signature().
> > If that was the case, the process die()'d. > > The other code paths that did signature verification relied entirely on the return code from check_commit_signature().
> > And signatures made with a good key, irregardless of its trust level, was considered valid by check_commit_signature(). > > This difference in behavior might induce users to erroneously assume that the trust level of a key in their keyring is always considered by Git, even for operations where it is not (e.g. during a verify-commit or verify-tag). > > The way it worked was by gpg-interface.c storing the result from the key/signature status and the lowest-two trust levels in the result member of the signature_check structure (the last of these status lines that were encountered got written to result).
> > These are documented in GPG under the subsection General status codes and Key related, respectively. > > The GPG documentation says the following on the TRUST_ status codes: > > > ----- > > > These are several similar status codes: > > > - TRUST_UNDEFINED > > - TRUST_NEVER > > - TRUST_MARGINAL [0 []] > > - TRUST_FULLY [0 []] > > - TRUST_ULTIMATE [0 []] > > > > For good signatures one of these status lines are emitted to indicate the validity of the key used to create the signature.
The error token values are currently only emitted by gpgsm. > > > > ----- > > My interpretation is that the trust level is conceptually different from the validity of the key and/or signature.
> > That seems to also have been the assumption of the old code in check_signature() where a result of 'G' (as in GOODSIG) and 'U' (as in TRUST_NEVER or TRUST_UNDEFINED) were both considered a success. > > The two cases where a result of 'U' had special meaning were in verify_merge_signature() (where this caused git to die()) and in format_commit_one() (where it affected the output of the %G? format specifier). > > I think it makes sense to refactor the processing of TRUST_ status lines such that users can configure a minimum trust level that is enforced globally, rather than have individual parts of git (e.g. merge) do it themselves (except for a grace period with backward compatibility). > > I also think it makes sense to not store the trust level in the same struct member as the key/signature status.
> > While the presence of a TRUST_ status code does imply that the signature is good (see the first paragraph in the included snippet above), as far as I can tell, the order of the status lines from GPG isn't well-defined; thus it would seem plausible that the trust level could be overwritten with the key/signature status if they were stored in the same member of the signature_check structure. > > This patch introduces a new configuration option: gpg.minTrustLevel.
> > It consolidates trust-level verification to gpg-interface.c and adds a new trust_level member to the signature_check structure. > > Backward-compatibility is maintained by introducing a special case in verify_merge_signature() such that if no user-configurable gpg.minTrustLevel is set, then the old behavior of rejecting TRUST_UNDEFINED and TRUST_NEVER is enforced.
> > If, on the other hand, gpg.minTrustLevel is set, then that value overrides the old behavior. > > Similarly, the %G? format specifier will continue show 'U' for signatures made with a key that has a trust level of TRUST_UNDEFINED or TRUST_NEVER, even though the 'U' character no longer exist in the result member of the signature_check structure.
> > A new format specifier, %GT, is also introduced for users that want to show all possible trust levels for a signature. > > Another approach would have been to simply drop the trust-level requirement in verify_merge_signature().
> > This would also have made the behavior consistent with other parts of git that perform signature verification.
> > However, requiring a minimum trust level for signing keys does seem to have a real-world use-case.
> > For example, the build system used by the Qubes OS project currently parses the raw output from verify-tag in order to assert a minimum trust level for keys used to sign git tags.

The git config gpg man page now includes:

> ## gpg.minTrustLevel:

> Specifies a minimum trust level for signature verification.
If this option is unset, then signature verification for merge operations require a key with at least marginal trust.
Other operations that perform signature verification require a key with at least undefined trust.
Setting this option overrides the required trust-level for all operations. Supported values, in increasing order of significance: > > * undefined > * never > * marginal > * fully > * ultimate


With Git 2.26 (Q1 2020), "git show" and others gave an object name in raw format in its error output, which has been corrected to give it in hex.

> ## show_one_mergetag: print non-parent in hex form.

> When a mergetag names a non-parent, which can occur after a shallow clone, its hash was previously printed as raw data.
Print it in hex form instead.

Tested with git -C shallow log --graph --show-signature -n1 plain-shallow after a git clone --depth 1 --no-local . shallow


With Git 2.27 (Q2 2020), the code to interface with GnuPG has been refactored.

See commit 6794898, commit f1e3df3 (04 Mar 2020) by Hans Jerry Illikainen (illikainen).
(Merged by Junio C Hamano -- gitster -- in commit fa82be9, 27 Mar 2020)

> ## gpg-interface: prefer check_signature() for GPG verification
> Signed-off-by: Hans Jerry Illikainen

> This commit refactors the use of verify_signed_buffer() outside of gpg-interface.c to use check_signature() instead.
> > It also turns verify_signed_buffer() into a file-local function since it's now only invoked internally by check_signature(). > > There were previously two globally scoped functions used in different parts of Git to perform GPG signature verification: verify_signed_buffer() and check_signature().
> > Now only check_signature() is used. > > The verify_signed_buffer() function doesn't guard against duplicate signatures as described by Michał Górny.
> > Instead it only ensures a non-erroneous exit code from GPG and the presence of at least one GOODSIG status field.
> > This stands in contrast with check_signature() that returns an error if more than one signature is encountered. > > The lower degree of verification makes the use of verify_signed_buffer() problematic if callers don't parse and validate the various parts of the GPG status message themselves.
> > And processing these messages seems like a task that should be reserved to gpg-interface.c with the function check_signature(). > > Furthermore, the use of verify_signed_buffer() makes it difficult to introduce new functionality that relies on the content of the GPG status lines. > > Now all operations that does signature verification share a single entry point to gpg-interface.c.
> > This makes it easier to propagate changed or additional functionality in GPG signature verification to all parts of Git, without having odd edge-cases that don't perform the same degree of verification.


With Git 2.31 (Q1 2021), signed commits and tags now allow verification of objects, whose two object names (one in SHA-1, the other in SHA-256) are both signed.

See commit 9b27b49, commit 88bce0e, commit 937032e, commit 482c119 (11 Feb 2021), and commit 1fb5cf0, commit 83dff3e (18 Jan 2021) by brian m. carlson (bk2204).
(Merged by Junio C Hamano -- gitster -- in commit 15af6e6, 22 Feb 2021)

> ## commit: ignore additional signatures when parsing signed commits
> Signed-off-by: brian m. carlson

> When we create a commit with multiple signatures, neither of these signatures includes the other.
> Consequently, when we produce the payload which has been signed so we can verify the commit, we must strip off any other signatures, or the payload will differ from what was signed.
> Do so, and in preparation for verifying with multiple algorithms, pass the algorithm we want to verify into parse_signed_commit.


Brandon proposes in the comments a git log alias, which shows key states:

[alias]
	lg = "!f() { \
		git log --all --color --graph --pretty=format:'%C(bold yellow)<sig>%G?</sig>%C(reset) %C(red)%h%C(reset) -%C(yellow)%d%C(reset) %s %C(green)(%cr) %C(blue)<%an>%C(reset)' | \
		sed \
		-e 's#<sig>G</sig>#Good#' \
		-e 's#<sig>B</sig>#\\nBAD \\nBAD \\nBAD \\nBAD \\nBAD#' \
		-e 's#<sig>U</sig>#Unknown#' \
		-e 's#<sig>X</sig>#Expired#' \
		-e 's#<sig>Y</sig>#Expired Key#' \
		-e 's#<sig>R</sig>#Revoked#' \
		-e 's#<sig>E</sig>#Missing Key#' \
		-e 's#<sig>N</sig>#None#' | \
		less -r; \
    }; f"

Solution 3 - Git

A cursory inspection of the code suggests that there is no such direct method.

All of the tests in the git source rely on grepping the output of git show (see t/t7510-signed-commit.sh for the tests).

You can customize the output using something like --pretty "%H %G?%" to make it easy to parse.

It appears you can ask git merge to verify a signature but again, its tests rely on grep (see t/t7612-merge-verify-signatures.sh). It does look like an invalid signature will cause git merge to exit with a bad signature, so you could potentially today hack around this by doing a test merge somewhere and throwing out that merge but that seems worse than just calling grep.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionlarsksView Question on Stackoverflow
Solution 1 - GittarlebView Answer on Stackoverflow
Solution 2 - GitVonCView Answer on Stackoverflow
Solution 3 - GitEmil SitView Answer on Stackoverflow