fast-(import|export): improve on commit signature output format
A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
signed-commits, 2025-03-10), added support for signed commits to
fast-export and fast-import.
When a signed commit is processed, fast-export can output either
"gpgsig sha1" or "gpgsig sha256" depending on whether the signed
commit uses the SHA-1 or SHA-256 Git object format.
However, this implementation has a number of limitations:
- the output format was not properly described in the documentation,
- the output format is not very informative as it doesn't even say
if the signature is an OpenPGP, an SSH, or an X509 signature,
- the implementation doesn't support having both one signature on
the SHA-1 object and one on the SHA-256 object.
Let's improve on these limitations by improving fast-export and
fast-import so that:
- all the signatures are exported,
- at most one signature on the SHA-1 object and one on the SHA-256
are imported,
- if there is more than one signature on the SHA-1 object or on
the SHA-256 object, fast-import emits a warning for each
additional signature,
- the output format is "gpgsig <git-hash-algo> <signature-format>",
where <git-hash-algo> is the Git object format as before, and
<signature-format> is the signature type ("openpgp", "x509",
"ssh" or "unknown"),
- the output is properly documented.
About the output format:
- <git-hash-algo> allows to know which representation of the commit
was signed (the SHA-1 or the SHA-256 version) which helps with
both signature verification and interoperability between repos
with different hash functions,
- <signature-format> helps tools that process the fast-export
stream, so they don't have to parse the ASCII armor to identify
the signature type.
It could be even better to be able to import more than one signature
on the SHA-1 object and on the SHA-256 object, but other parts of
Git don't handle that well for now, so this is left for future
improvements.
Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Christian Couder committedJul 9, 2025 at 16:12 UTCb5b3ddbe5c56c7ded95e7c47c985dc6d61f73ea0
7 files changed+312-44
Documentation/git-fast-export.adoc
+17
index 43bbb4f63c..297b57bb2e 100644--- a/Documentation/git-fast-export.adoc+++ b/Documentation/git-fast-export.adoc@@ -50,6 +50,23 @@ resulting tag will have an invalid signature. is the same as how earlier versions of this command without this option behaved. ++When exported, a signature starts with:+++gpgsig <git-hash-algo> <signature-format>+++where <git-hash-algo> is the Git object hash so either "sha1" or+"sha256", and <signature-format> is the signature type, so "openpgp",+"x509", "ssh" or "unknown".+++For example, an OpenPGP signature on a SHA-1 commit starts with+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit+starts with `gpgsig sha256 ssh`.+++While all the signatures of a commit are exported, an importer may+choose to accept only some of them. For example+linkgit:git-fast-import[1] currently stores at most one signature per+Git hash algorithm in each commit.++ NOTE: This is highly experimental and the format of the data stream may change in the future without compatibility guarantees.
Documentation/git-fast-import.adoc
+32-6
index 250d866652..d232784200 100644--- a/Documentation/git-fast-import.adoc+++ b/Documentation/git-fast-import.adoc@@ -445,7 +445,7 @@ one). original-oid? ('author' (SP <name>)? SP LT <email> GT SP <when> LF)? 'committer' (SP <name>)? SP LT <email> GT SP <when> LF- ('gpgsig' SP <alg> LF data)?+ ('gpgsig' SP <algo> SP <format> LF data)? ('encoding' SP <encoding> LF)? data ('from' SP <commit-ish> LF)?@@ -518,13 +518,39 @@ their syntax. ^^^^^^^^ The optional `gpgsig` command is used to include a PGP/GPG signature-that signs the commit data.+or other cryptographic signature that signs the commit data.-Here <alg> specifies which hashing algorithm is used for this-signature, either `sha1` or `sha256`.+....+ 'gpgsig' SP <git-hash-algo> SP <signature-format> LF data+....++The `gpgsig` command takes two arguments:++* `<git-hash-algo>` specifies which Git object format this signature+ applies to, either `sha1` or `sha256`. This allows to know which+ representation of the commit was signed (the SHA-1 or the SHA-256+ version) which helps with both signature verification and+ interoperability between repos with different hash functions.++* `<signature-format>` specifies the type of signature, such as+ `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for+ tools that process the stream, so they don't have to parse the ASCII+ armor to identify the signature type.++A commit may have at most one signature for the SHA-1 object format+(stored in the "gpgsig" header) and one for the SHA-256 object format+(stored in the "gpgsig-sha256" header).++See below for a detailed description of the `data` command which+contains the raw signature data.++Signatures are not yet checked in the current implementation+though. (Already setting the `extensions.compatObjectFormat`+configuration option might help with verifying both SHA-1 and SHA-256+object format signatures when it will be implemented.)-NOTE: This is highly experimental and the format of the data stream may-change in the future without compatibility guarantees.+NOTE: This is highly experimental and the format of the `gpgsig`+command may change in the future without compatibility guarantees. `encoding` ^^^^^^^^^^
index e09f12e8d0..60ddf8bbfa 100644--- a/gpg-interface.h+++ b/gpg-interface.h@@ -47,6 +47,18 @@ struct signature_check { void signature_check_clear(struct signature_check *sigc);+/*+ * Return the format of the signature (like "openpgp", "x509", "ssh"+ * or "unknown").+ */+const char *get_signature_format(const char *buf);++/*+ * Is the signature format valid (like "openpgp", "x509", "ssh" or+ * "unknown")+ */+int valid_signature_format(const char *format);+ /* * Look at a GPG signed tag object. If such a signature exists, store it in * signature and the signed content in payload. Return 1 if a signature was
t/t9350-fast-export.sh
+100-2
index 76619765fc..46700dbc40 100755--- a/t/t9350-fast-export.sh+++ b/t/t9350-fast-export.sh@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' ' test_expect_success GPG 'signed-commits=verbatim' ' git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&- grep "^gpgsig sha" output &&+ test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output && grep "encoding ISO-8859-1" output && ( cd new &&@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' ' test_expect_success GPG 'signed-commits=warn-verbatim' ' git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&- grep "^gpgsig sha" output &&+ test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output && grep "encoding ISO-8859-1" output && test -s err && (@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' ' '+test_expect_success GPGSM 'setup X.509 signed commit' '++ git checkout -b x509-signing main &&+ test_config gpg.format x509 &&+ test_config user.signingkey $GIT_COMMITTER_EMAIL &&+ echo "X.509 content" >file &&+ git add file &&+ git commit -S -m "X.509 signed commit" &&+ X509_COMMIT=$(git rev-parse HEAD) &&+ git checkout main++'++test_expect_success GPGSM 'round-trip X.509 signed commit' '++ git fast-export --signed-commits=verbatim x509-signing >output &&+ test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&+ (+ cd new &&+ git fast-import &&+ git cat-file commit refs/heads/x509-signing >actual &&+ grep "^gpgsig" actual &&+ IMPORTED=$(git rev-parse refs/heads/x509-signing) &&+ test $X509_COMMIT = $IMPORTED+ ) <output++'++test_expect_success GPGSSH 'setup SSH signed commit' '++ git checkout -b ssh-signing main &&+ test_config gpg.format ssh &&+ test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&+ echo "SSH content" >file &&+ git add file &&+ git commit -S -m "SSH signed commit" &&+ SSH_COMMIT=$(git rev-parse HEAD) &&+ git checkout main++'++test_expect_success GPGSSH 'round-trip SSH signed commit' '++ git fast-export --signed-commits=verbatim ssh-signing >output &&+ test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&+ (+ cd new &&+ git fast-import &&+ git cat-file commit refs/heads/ssh-signing >actual &&+ grep "^gpgsig" actual &&+ IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&+ test $SSH_COMMIT = $IMPORTED+ ) <output++'+ test_expect_success 'setup submodule' ' test_config_global protocol.file.allow always &&@@ -905,4 +961,46 @@ test_expect_success 'fast-export handles --end-of-options' ' test_cmp expect actual '+test_expect_success GPG 'setup a commit with dual signatures on its SHA-1 and SHA-256 formats' '+ # Create a signed SHA-256 commit+ git init --object-format=sha256 explicit-sha256 &&+ git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&+ git -C explicit-sha256 checkout -b dual-signed &&+ test_commit -C explicit-sha256 A &&+ echo B >explicit-sha256/B &&+ git -C explicit-sha256 add B &&+ test_tick &&+ git -C explicit-sha256 commit -S -m "signed" B &&+ SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&++ # Create the corresponding SHA-1 commit+ SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&++ # Check that the resulting SHA-1 commit has both signatures+ echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&+ test_grep -E "^gpgsig " out &&+ test_grep -E "^gpgsig-sha256 " out+'++test_expect_success GPG 'export and import of doubly signed commit' '+ git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&+ test_grep -E "^gpgsig sha1 openpgp" output &&+ test_grep -E "^gpgsig sha256 openpgp" output &&++ (+ cd new &&+ git fast-import &&+ git cat-file commit refs/heads/dual-signed >actual &&+ test_grep -E "^gpgsig " actual &&+ test_grep -E "^gpgsig-sha256 " actual &&+ IMPORTED=$(git rev-parse refs/heads/dual-signed) &&+ if test "$GIT_DEFAULT_HASH" = "sha1"+ then+ test $SHA1_B = $IMPORTED+ else+ test $SHA256_B = $IMPORTED+ fi+ ) <output+'+ test_done