Verifying signed git commits?
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.
A cursory inspection of the code suggests that there is no such direct method.
All of the tests in the git source rely on grep
ping 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.
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 makeverify-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.
Sinceverify-tag
existed earlier, add a failing test to haveverify-commit
shareverify-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
orERRSIG
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
togit 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 forverifySignatures
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 fromgpg
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
" (orgpg.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 optionSigned-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
orTRUST_UNDEFINED
inverify_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
orverify-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 theresult
member of thesignature_check
structure (the last of these status lines that were encountered got written toresult
).These are documented in GPG under the subsection
General status codes
andKey related
, respectively.The GPG documentation says the following on the
TRUST_ status
codes:These are several similar status codes:
- TRUST_UNDEFINED <error_token> - TRUST_NEVER <error_token> - TRUST_MARGINAL [0 [<validation_model>]] - TRUST_FULLY [0 [<validation_model>]] - TRUST_ULTIMATE [0 [<validation_model>]]
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 inGOODSIG
) and 'U
' (as inTRUST_NEVER
orTRUST_UNDEFINED)
were both considered a success.The two cases where a result of '
U
' had special meaning were inverify_merge_signature()
(where this causedgit
todie()
) and informat_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 ofgit
(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 thesignature_check
structure.This patch introduces a new configuration option:
gpg.minTrustLevel
.It consolidates trust-level verification to
gpg-interface.c
and adds a newtrust_level
member to thesignature_check
structure.Backward-compatibility is maintained by introducing a special case in
verify_merge_signature()
such that if no user-configurablegpg.minTrustLevel
is set, then the old behavior of rejectingTRUST_UNDEFINED
andTRUST_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 ofTRUST_UNDEFINED
orTRUST_NEVER,
even though the 'U
' character no longer exist in theresult
member of thesignature_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 leastmarginal
trust.
Other operations that perform signature verification require a key with at leastundefined
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
: prefercheck_signature()
for GPG verificationSigned-off-by: Hans Jerry Illikainen
This commit refactors the use of
verify_signed_buffer()
outside ofgpg-interface.c
to usecheck_signature()
instead.It also turns
verify_signed_buffer()
into a file-local function since it's now only invoked internally bycheck_signature()
.There were previously two globally scoped functions used in different parts of Git to perform GPG signature verification:
verify_signed_buffer()
andcheck_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 functioncheck_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 commitsSigned-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 intoparse_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"