summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2025-07-23 15:45:16 -0700
committerJunio C Hamano <gitster@pobox.com>2025-07-23 15:45:16 -0700
commit5216bcbc845023fdea43b6d56f3634c490096838 (patch)
tree4f5984b483fbbb92da576fd4a6f9f03fae1ec54e
parent98813265b360ebc59371d7d32efa47eb24adda15 (diff)
parentb5b3ddbe5c56c7ded95e7c47c985dc6d61f73ea0 (diff)
Merge branch 'cc/fast-import-export-signature-names'
Clean up the way how signature on commit objects are exported to and imported from fast-import stream. * cc/fast-import-export-signature-names: fast-(import|export): improve on commit signature output format
-rw-r--r--Documentation/git-fast-export.adoc17
-rw-r--r--Documentation/git-fast-import.adoc38
-rw-r--r--builtin/fast-export.c62
-rw-r--r--builtin/fast-import.c113
-rw-r--r--gpg-interface.c12
-rw-r--r--gpg-interface.h12
-rwxr-xr-xt/t9350-fast-export.sh102
7 files changed, 312 insertions, 44 deletions
diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
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.
diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
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`
^^^^^^^^^^
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index 6a3a17a8cd..f4169dc5f3 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -29,6 +29,7 @@
#include "quote.h"
#include "remote.h"
#include "blob.h"
+#include "gpg-interface.h"
static const char *const fast_export_usage[] = {
N_("git fast-export [<rev-list-opts>]"),
@@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,
return strbuf_detach(&val, NULL);
}
+static void print_signature(const char *signature, const char *object_hash)
+{
+ if (!signature)
+ return;
+
+ printf("gpgsig %s %s\ndata %u\n%s\n",
+ object_hash,
+ get_signature_format(signature),
+ (unsigned)strlen(signature),
+ signature);
+}
+
+static const char *append_signatures_for_header(struct string_list *signatures,
+ const char *pos,
+ const char *header,
+ const char *object_hash)
+{
+ const char *signature;
+ const char *start = pos;
+ const char *end = pos;
+
+ while ((signature = find_commit_multiline_header(start + 1,
+ header,
+ &end))) {
+ string_list_append(signatures, signature)->util = (void *)object_hash;
+ free((char *)signature);
+ start = end;
+ }
+
+ return end;
+}
+
static void handle_commit(struct commit *commit, struct rev_info *rev,
struct string_list *paths_of_changed_objects)
{
@@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
const char *author, *author_end, *committer, *committer_end;
const char *encoding = NULL;
size_t encoding_len;
- const char *signature_alg = NULL, *signature = NULL;
+ struct string_list signatures = STRING_LIST_INIT_DUP;
const char *message;
char *reencoded = NULL;
struct commit_list *p;
@@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
}
if (*commit_buffer_cursor == '\n') {
- if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
- signature_alg = "sha1";
- else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
- signature_alg = "sha256";
+ const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
+ "gpgsig", "sha1");
+ const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
+ "gpgsig-sha256", "sha256");
+ commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
}
message = strstr(commit_buffer_cursor, "\n\n");
@@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
printf("%.*s\n%.*s\n",
(int)(author_end - author), author,
(int)(committer_end - committer), committer);
- if (signature) {
+ if (signatures.nr) {
switch (signed_commit_mode) {
case SIGN_ABORT:
die("encountered signed commit %s; use "
"--signed-commits=<mode> to handle it",
oid_to_hex(&commit->object.oid));
case SIGN_WARN_VERBATIM:
- warning("exporting signed commit %s",
- oid_to_hex(&commit->object.oid));
+ warning("exporting %"PRIuMAX" signature(s) for commit %s",
+ (uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
/* fallthru */
case SIGN_VERBATIM:
- printf("gpgsig %s\ndata %u\n%s",
- signature_alg,
- (unsigned)strlen(signature),
- signature);
+ for (size_t i = 0; i < signatures.nr; i++) {
+ struct string_list_item *item = &signatures.items[i];
+ print_signature(item->string, item->util);
+ }
break;
case SIGN_WARN_STRIP:
- warning("stripping signature from commit %s",
+ warning("stripping signature(s) from commit %s",
oid_to_hex(&commit->object.oid));
/* fallthru */
case SIGN_STRIP:
break;
}
- free((char *)signature);
+ string_list_clear(&signatures, 0);
}
if (!reencoded && encoding)
printf("encoding %.*s\n", (int)encoding_len, encoding);
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index b1389c5921..6e7d0c3449 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -29,6 +29,7 @@
#include "commit-reach.h"
#include "khash.h"
#include "date.h"
+#include "gpg-interface.h"
#define PACK_ID_BITS 16
#define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2716,15 +2717,82 @@ static struct hash_list *parse_merge(unsigned int *count)
return list;
}
+struct signature_data {
+ char *hash_algo; /* "sha1" or "sha256" */
+ char *sig_format; /* "openpgp", "x509", "ssh", or "unknown" */
+ struct strbuf data; /* The actual signature data */
+};
+
+static void parse_one_signature(struct signature_data *sig, const char *v)
+{
+ char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
+ char *space = strchr(args, ' ');
+
+ if (!space)
+ die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
+ "got 'gpgsig %s'", args);
+ *space = '\0';
+
+ sig->hash_algo = args;
+ sig->sig_format = space + 1;
+
+ /* Validate hash algorithm */
+ if (strcmp(sig->hash_algo, "sha1") &&
+ strcmp(sig->hash_algo, "sha256"))
+ die("Unknown git hash algorithm in gpgsig: '%s'", sig->hash_algo);
+
+ /* Validate signature format */
+ if (!valid_signature_format(sig->sig_format))
+ die("Invalid signature format in gpgsig: '%s'", sig->sig_format);
+ if (!strcmp(sig->sig_format, "unknown"))
+ warning("'unknown' signature format in gpgsig");
+
+ /* Read signature data */
+ read_next_command();
+ parse_data(&sig->data, 0, NULL);
+}
+
+static void add_gpgsig_to_commit(struct strbuf *commit_data,
+ const char *header,
+ struct signature_data *sig)
+{
+ struct string_list siglines = STRING_LIST_INIT_NODUP;
+
+ if (!sig->hash_algo)
+ return;
+
+ strbuf_addstr(commit_data, header);
+ string_list_split_in_place(&siglines, sig->data.buf, "\n", -1);
+ strbuf_add_separated_string_list(commit_data, "\n ", &siglines);
+ strbuf_addch(commit_data, '\n');
+ string_list_clear(&siglines, 1);
+ strbuf_release(&sig->data);
+ free(sig->hash_algo);
+}
+
+static void store_signature(struct signature_data *stored_sig,
+ struct signature_data *new_sig,
+ const char *hash_type)
+{
+ if (stored_sig->hash_algo) {
+ warning("multiple %s signatures found, "
+ "ignoring additional signature",
+ hash_type);
+ strbuf_release(&new_sig->data);
+ free(new_sig->hash_algo);
+ } else {
+ *stored_sig = *new_sig;
+ }
+}
+
static void parse_new_commit(const char *arg)
{
- static struct strbuf sig = STRBUF_INIT;
static struct strbuf msg = STRBUF_INIT;
- struct string_list siglines = STRING_LIST_INIT_NODUP;
+ struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };
+ struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };
struct branch *b;
char *author = NULL;
char *committer = NULL;
- char *sig_alg = NULL;
char *encoding = NULL;
struct hash_list *merge_list = NULL;
unsigned int merge_count;
@@ -2748,13 +2816,23 @@ static void parse_new_commit(const char *arg)
}
if (!committer)
die("Expected committer but didn't get one");
- if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
- sig_alg = xstrdup(v);
- read_next_command();
- parse_data(&sig, 0, NULL);
+
+ /* Process signatures (up to 2: one "sha1" and one "sha256") */
+ while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
+ struct signature_data sig = { NULL, NULL, STRBUF_INIT };
+
+ parse_one_signature(&sig, v);
+
+ if (!strcmp(sig.hash_algo, "sha1"))
+ store_signature(&sig_sha1, &sig, "SHA-1");
+ else if (!strcmp(sig.hash_algo, "sha256"))
+ store_signature(&sig_sha256, &sig, "SHA-256");
+ else
+ BUG("parse_one_signature() returned unknown hash algo");
+
read_next_command();
- } else
- strbuf_setlen(&sig, 0);
+ }
+
if (skip_prefix(command_buf.buf, "encoding ", &v)) {
encoding = xstrdup(v);
read_next_command();
@@ -2828,23 +2906,14 @@ static void parse_new_commit(const char *arg)
strbuf_addf(&new_data,
"encoding %s\n",
encoding);
- if (sig_alg) {
- if (!strcmp(sig_alg, "sha1"))
- strbuf_addstr(&new_data, "gpgsig ");
- else if (!strcmp(sig_alg, "sha256"))
- strbuf_addstr(&new_data, "gpgsig-sha256 ");
- else
- die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
- string_list_split_in_place(&siglines, sig.buf, "\n", -1);
- strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
- strbuf_addch(&new_data, '\n');
- }
+
+ add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
+ add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
+
strbuf_addch(&new_data, '\n');
strbuf_addbuf(&new_data, &msg);
- string_list_clear(&siglines, 1);
free(author);
free(committer);
- free(sig_alg);
free(encoding);
if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))
diff --git a/gpg-interface.c b/gpg-interface.c
index 12e1d5910e..d26c7135b0 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
return NULL;
}
+const char *get_signature_format(const char *buf)
+{
+ struct gpg_format *format = get_format_by_sig(buf);
+ return format ? format->name : "unknown";
+}
+
+int valid_signature_format(const char *format)
+{
+ return (!!get_format_by_name(format) ||
+ !strcmp(format, "unknown"));
+}
+
void signature_check_clear(struct signature_check *sigc)
{
FREE_AND_NULL(sigc->payload);
diff --git a/gpg-interface.h b/gpg-interface.h
index e09f12e8d0..60ddf8bbfa 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -48,6 +48,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
* found, and 0 otherwise.
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
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