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
This commit is contained in:
Junio C Hamano
2025-07-23 15:45:16 -07:00
7 changed files with 312 additions and 44 deletions

View File

@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
is the same as how earlier versions of this command without is the same as how earlier versions of this command without
this option behaved. 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 NOTE: This is highly experimental and the format of the data stream may
change in the future without compatibility guarantees. change in the future without compatibility guarantees.

View File

@@ -445,7 +445,7 @@ one).
original-oid? original-oid?
('author' (SP <name>)? SP LT <email> GT SP <when> LF)? ('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
'committer' (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)? ('encoding' SP <encoding> LF)?
data data
('from' SP <commit-ish> LF)? ('from' SP <commit-ish> LF)?
@@ -518,13 +518,39 @@ their syntax.
^^^^^^^^ ^^^^^^^^
The optional `gpgsig` command is used to include a PGP/GPG signature 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
....
NOTE: This is highly experimental and the format of the data stream may The `gpgsig` command takes two arguments:
change in the future without compatibility guarantees.
* `<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 `gpgsig`
command may change in the future without compatibility guarantees.
`encoding` `encoding`
^^^^^^^^^^ ^^^^^^^^^^

View File

@@ -29,6 +29,7 @@
#include "quote.h" #include "quote.h"
#include "remote.h" #include "remote.h"
#include "blob.h" #include "blob.h"
#include "gpg-interface.h"
static const char *const fast_export_usage[] = { static const char *const fast_export_usage[] = {
N_("git fast-export [<rev-list-opts>]"), 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); 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, static void handle_commit(struct commit *commit, struct rev_info *rev,
struct string_list *paths_of_changed_objects) 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 *author, *author_end, *committer, *committer_end;
const char *encoding = NULL; const char *encoding = NULL;
size_t encoding_len; size_t encoding_len;
const char *signature_alg = NULL, *signature = NULL; struct string_list signatures = STRING_LIST_INIT_DUP;
const char *message; const char *message;
char *reencoded = NULL; char *reencoded = NULL;
struct commit_list *p; 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 (*commit_buffer_cursor == '\n') {
if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor))) const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
signature_alg = "sha1"; "gpgsig", "sha1");
else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor))) const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
signature_alg = "sha256"; "gpgsig-sha256", "sha256");
commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
} }
message = strstr(commit_buffer_cursor, "\n\n"); 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", printf("%.*s\n%.*s\n",
(int)(author_end - author), author, (int)(author_end - author), author,
(int)(committer_end - committer), committer); (int)(committer_end - committer), committer);
if (signature) { if (signatures.nr) {
switch (signed_commit_mode) { switch (signed_commit_mode) {
case SIGN_ABORT: case SIGN_ABORT:
die("encountered signed commit %s; use " die("encountered signed commit %s; use "
"--signed-commits=<mode> to handle it", "--signed-commits=<mode> to handle it",
oid_to_hex(&commit->object.oid)); oid_to_hex(&commit->object.oid));
case SIGN_WARN_VERBATIM: case SIGN_WARN_VERBATIM:
warning("exporting signed commit %s", warning("exporting %"PRIuMAX" signature(s) for commit %s",
oid_to_hex(&commit->object.oid)); (uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
/* fallthru */ /* fallthru */
case SIGN_VERBATIM: case SIGN_VERBATIM:
printf("gpgsig %s\ndata %u\n%s", for (size_t i = 0; i < signatures.nr; i++) {
signature_alg, struct string_list_item *item = &signatures.items[i];
(unsigned)strlen(signature), print_signature(item->string, item->util);
signature); }
break; break;
case SIGN_WARN_STRIP: case SIGN_WARN_STRIP:
warning("stripping signature from commit %s", warning("stripping signature(s) from commit %s",
oid_to_hex(&commit->object.oid)); oid_to_hex(&commit->object.oid));
/* fallthru */ /* fallthru */
case SIGN_STRIP: case SIGN_STRIP:
break; break;
} }
free((char *)signature); string_list_clear(&signatures, 0);
} }
if (!reencoded && encoding) if (!reencoded && encoding)
printf("encoding %.*s\n", (int)encoding_len, encoding); printf("encoding %.*s\n", (int)encoding_len, encoding);

View File

@@ -29,6 +29,7 @@
#include "commit-reach.h" #include "commit-reach.h"
#include "khash.h" #include "khash.h"
#include "date.h" #include "date.h"
#include "gpg-interface.h"
#define PACK_ID_BITS 16 #define PACK_ID_BITS 16
#define MAX_PACK_ID ((1<<PACK_ID_BITS)-1) #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2716,15 +2717,82 @@ static struct hash_list *parse_merge(unsigned int *count)
return list; 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 void parse_new_commit(const char *arg)
{ {
static struct strbuf sig = STRBUF_INIT;
static struct strbuf msg = 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; struct branch *b;
char *author = NULL; char *author = NULL;
char *committer = NULL; char *committer = NULL;
char *sig_alg = NULL;
char *encoding = NULL; char *encoding = NULL;
struct hash_list *merge_list = NULL; struct hash_list *merge_list = NULL;
unsigned int merge_count; unsigned int merge_count;
@@ -2748,13 +2816,23 @@ static void parse_new_commit(const char *arg)
} }
if (!committer) if (!committer)
die("Expected committer but didn't get one"); die("Expected committer but didn't get one");
if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
sig_alg = xstrdup(v); /* 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(); read_next_command();
parse_data(&sig, 0, NULL); }
read_next_command();
} else
strbuf_setlen(&sig, 0);
if (skip_prefix(command_buf.buf, "encoding ", &v)) { if (skip_prefix(command_buf.buf, "encoding ", &v)) {
encoding = xstrdup(v); encoding = xstrdup(v);
read_next_command(); read_next_command();
@@ -2828,23 +2906,14 @@ static void parse_new_commit(const char *arg)
strbuf_addf(&new_data, strbuf_addf(&new_data,
"encoding %s\n", "encoding %s\n",
encoding); encoding);
if (sig_alg) {
if (!strcmp(sig_alg, "sha1")) add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
strbuf_addstr(&new_data, "gpgsig "); add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
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');
}
strbuf_addch(&new_data, '\n'); strbuf_addch(&new_data, '\n');
strbuf_addbuf(&new_data, &msg); strbuf_addbuf(&new_data, &msg);
string_list_clear(&siglines, 1);
free(author); free(author);
free(committer); free(committer);
free(sig_alg);
free(encoding); free(encoding);
if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark)) if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))

View File

@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
return NULL; 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) void signature_check_clear(struct signature_check *sigc)
{ {
FREE_AND_NULL(sigc->payload); FREE_AND_NULL(sigc->payload);

View File

@@ -47,6 +47,18 @@ struct signature_check {
void signature_check_clear(struct signature_check *sigc); 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 * 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 * signature and the signed content in payload. Return 1 if a signature was

View File

@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '
test_expect_success GPG 'signed-commits=verbatim' ' test_expect_success GPG 'signed-commits=verbatim' '
git fast-export --signed-commits=verbatim --reencode=no commit-signing >output && 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 && grep "encoding ISO-8859-1" output &&
( (
cd new && cd new &&
@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
test_expect_success GPG 'signed-commits=warn-verbatim' ' test_expect_success GPG 'signed-commits=warn-verbatim' '
git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err && 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 && grep "encoding ISO-8859-1" output &&
test -s err && 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_expect_success 'setup submodule' '
test_config_global protocol.file.allow always && 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_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 test_done