Skip to content

Commit b5b3ddb

Browse files
chriscoolgitster
authored andcommitted
fast-(import|export): improve on commit signature output format
A recent commit, d9cb0e6 (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 <[email protected]> Helped-by: Elijah Newren <[email protected]> Signed-off-by: Christian Couder <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent cb3b403 commit b5b3ddb

File tree

7 files changed

+312
-44
lines changed

7 files changed

+312
-44
lines changed

Documentation/git-fast-export.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
5050
is the same as how earlier versions of this command without
5151
this option behaved.
5252
+
53+
When exported, a signature starts with:
54+
+
55+
gpgsig <git-hash-algo> <signature-format>
56+
+
57+
where <git-hash-algo> is the Git object hash so either "sha1" or
58+
"sha256", and <signature-format> is the signature type, so "openpgp",
59+
"x509", "ssh" or "unknown".
60+
+
61+
For example, an OpenPGP signature on a SHA-1 commit starts with
62+
`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
63+
starts with `gpgsig sha256 ssh`.
64+
+
65+
While all the signatures of a commit are exported, an importer may
66+
choose to accept only some of them. For example
67+
linkgit:git-fast-import[1] currently stores at most one signature per
68+
Git hash algorithm in each commit.
69+
+
5370
NOTE: This is highly experimental and the format of the data stream may
5471
change in the future without compatibility guarantees.
5572

Documentation/git-fast-import.adoc

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ one).
445445
original-oid?
446446
('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
447447
'committer' (SP <name>)? SP LT <email> GT SP <when> LF
448-
('gpgsig' SP <alg> LF data)?
448+
('gpgsig' SP <algo> SP <format> LF data)?
449449
('encoding' SP <encoding> LF)?
450450
data
451451
('from' SP <commit-ish> LF)?
@@ -518,13 +518,39 @@ their syntax.
518518
^^^^^^^^
519519

520520
The optional `gpgsig` command is used to include a PGP/GPG signature
521-
that signs the commit data.
521+
or other cryptographic signature that signs the commit data.
522522

523-
Here <alg> specifies which hashing algorithm is used for this
524-
signature, either `sha1` or `sha256`.
523+
....
524+
'gpgsig' SP <git-hash-algo> SP <signature-format> LF data
525+
....
526+
527+
The `gpgsig` command takes two arguments:
528+
529+
* `<git-hash-algo>` specifies which Git object format this signature
530+
applies to, either `sha1` or `sha256`. This allows to know which
531+
representation of the commit was signed (the SHA-1 or the SHA-256
532+
version) which helps with both signature verification and
533+
interoperability between repos with different hash functions.
534+
535+
* `<signature-format>` specifies the type of signature, such as
536+
`openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
537+
tools that process the stream, so they don't have to parse the ASCII
538+
armor to identify the signature type.
539+
540+
A commit may have at most one signature for the SHA-1 object format
541+
(stored in the "gpgsig" header) and one for the SHA-256 object format
542+
(stored in the "gpgsig-sha256" header).
543+
544+
See below for a detailed description of the `data` command which
545+
contains the raw signature data.
546+
547+
Signatures are not yet checked in the current implementation
548+
though. (Already setting the `extensions.compatObjectFormat`
549+
configuration option might help with verifying both SHA-1 and SHA-256
550+
object format signatures when it will be implemented.)
525551

526-
NOTE: This is highly experimental and the format of the data stream may
527-
change in the future without compatibility guarantees.
552+
NOTE: This is highly experimental and the format of the `gpgsig`
553+
command may change in the future without compatibility guarantees.
528554

529555
`encoding`
530556
^^^^^^^^^^

builtin/fast-export.c

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "quote.h"
3030
#include "remote.h"
3131
#include "blob.h"
32+
#include "gpg-interface.h"
3233

3334
static const char *const fast_export_usage[] = {
3435
N_("git fast-export [<rev-list-opts>]"),
@@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,
652653
return strbuf_detach(&val, NULL);
653654
}
654655

656+
static void print_signature(const char *signature, const char *object_hash)
657+
{
658+
if (!signature)
659+
return;
660+
661+
printf("gpgsig %s %s\ndata %u\n%s\n",
662+
object_hash,
663+
get_signature_format(signature),
664+
(unsigned)strlen(signature),
665+
signature);
666+
}
667+
668+
static const char *append_signatures_for_header(struct string_list *signatures,
669+
const char *pos,
670+
const char *header,
671+
const char *object_hash)
672+
{
673+
const char *signature;
674+
const char *start = pos;
675+
const char *end = pos;
676+
677+
while ((signature = find_commit_multiline_header(start + 1,
678+
header,
679+
&end))) {
680+
string_list_append(signatures, signature)->util = (void *)object_hash;
681+
free((char *)signature);
682+
start = end;
683+
}
684+
685+
return end;
686+
}
687+
655688
static void handle_commit(struct commit *commit, struct rev_info *rev,
656689
struct string_list *paths_of_changed_objects)
657690
{
@@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
660693
const char *author, *author_end, *committer, *committer_end;
661694
const char *encoding = NULL;
662695
size_t encoding_len;
663-
const char *signature_alg = NULL, *signature = NULL;
696+
struct string_list signatures = STRING_LIST_INIT_DUP;
664697
const char *message;
665698
char *reencoded = NULL;
666699
struct commit_list *p;
@@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
700733
}
701734

702735
if (*commit_buffer_cursor == '\n') {
703-
if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
704-
signature_alg = "sha1";
705-
else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
706-
signature_alg = "sha256";
736+
const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
737+
"gpgsig", "sha1");
738+
const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
739+
"gpgsig-sha256", "sha256");
740+
commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
707741
}
708742

709743
message = strstr(commit_buffer_cursor, "\n\n");
@@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
769803
printf("%.*s\n%.*s\n",
770804
(int)(author_end - author), author,
771805
(int)(committer_end - committer), committer);
772-
if (signature) {
806+
if (signatures.nr) {
773807
switch (signed_commit_mode) {
774808
case SIGN_ABORT:
775809
die("encountered signed commit %s; use "
776810
"--signed-commits=<mode> to handle it",
777811
oid_to_hex(&commit->object.oid));
778812
case SIGN_WARN_VERBATIM:
779-
warning("exporting signed commit %s",
780-
oid_to_hex(&commit->object.oid));
813+
warning("exporting %"PRIuMAX" signature(s) for commit %s",
814+
(uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
781815
/* fallthru */
782816
case SIGN_VERBATIM:
783-
printf("gpgsig %s\ndata %u\n%s",
784-
signature_alg,
785-
(unsigned)strlen(signature),
786-
signature);
817+
for (size_t i = 0; i < signatures.nr; i++) {
818+
struct string_list_item *item = &signatures.items[i];
819+
print_signature(item->string, item->util);
820+
}
787821
break;
788822
case SIGN_WARN_STRIP:
789-
warning("stripping signature from commit %s",
823+
warning("stripping signature(s) from commit %s",
790824
oid_to_hex(&commit->object.oid));
791825
/* fallthru */
792826
case SIGN_STRIP:
793827
break;
794828
}
795-
free((char *)signature);
829+
string_list_clear(&signatures, 0);
796830
}
797831
if (!reencoded && encoding)
798832
printf("encoding %.*s\n", (int)encoding_len, encoding);

builtin/fast-import.c

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "commit-reach.h"
3030
#include "khash.h"
3131
#include "date.h"
32+
#include "gpg-interface.h"
3233

3334
#define PACK_ID_BITS 16
3435
#define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2718,15 +2719,82 @@ static struct hash_list *parse_merge(unsigned int *count)
27182719
return list;
27192720
}
27202721

2722+
struct signature_data {
2723+
char *hash_algo; /* "sha1" or "sha256" */
2724+
char *sig_format; /* "openpgp", "x509", "ssh", or "unknown" */
2725+
struct strbuf data; /* The actual signature data */
2726+
};
2727+
2728+
static void parse_one_signature(struct signature_data *sig, const char *v)
2729+
{
2730+
char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
2731+
char *space = strchr(args, ' ');
2732+
2733+
if (!space)
2734+
die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
2735+
"got 'gpgsig %s'", args);
2736+
*space = '\0';
2737+
2738+
sig->hash_algo = args;
2739+
sig->sig_format = space + 1;
2740+
2741+
/* Validate hash algorithm */
2742+
if (strcmp(sig->hash_algo, "sha1") &&
2743+
strcmp(sig->hash_algo, "sha256"))
2744+
die("Unknown git hash algorithm in gpgsig: '%s'", sig->hash_algo);
2745+
2746+
/* Validate signature format */
2747+
if (!valid_signature_format(sig->sig_format))
2748+
die("Invalid signature format in gpgsig: '%s'", sig->sig_format);
2749+
if (!strcmp(sig->sig_format, "unknown"))
2750+
warning("'unknown' signature format in gpgsig");
2751+
2752+
/* Read signature data */
2753+
read_next_command();
2754+
parse_data(&sig->data, 0, NULL);
2755+
}
2756+
2757+
static void add_gpgsig_to_commit(struct strbuf *commit_data,
2758+
const char *header,
2759+
struct signature_data *sig)
2760+
{
2761+
struct string_list siglines = STRING_LIST_INIT_NODUP;
2762+
2763+
if (!sig->hash_algo)
2764+
return;
2765+
2766+
strbuf_addstr(commit_data, header);
2767+
string_list_split_in_place(&siglines, sig->data.buf, "\n", -1);
2768+
strbuf_add_separated_string_list(commit_data, "\n ", &siglines);
2769+
strbuf_addch(commit_data, '\n');
2770+
string_list_clear(&siglines, 1);
2771+
strbuf_release(&sig->data);
2772+
free(sig->hash_algo);
2773+
}
2774+
2775+
static void store_signature(struct signature_data *stored_sig,
2776+
struct signature_data *new_sig,
2777+
const char *hash_type)
2778+
{
2779+
if (stored_sig->hash_algo) {
2780+
warning("multiple %s signatures found, "
2781+
"ignoring additional signature",
2782+
hash_type);
2783+
strbuf_release(&new_sig->data);
2784+
free(new_sig->hash_algo);
2785+
} else {
2786+
*stored_sig = *new_sig;
2787+
}
2788+
}
2789+
27212790
static void parse_new_commit(const char *arg)
27222791
{
2723-
static struct strbuf sig = STRBUF_INIT;
27242792
static struct strbuf msg = STRBUF_INIT;
2725-
struct string_list siglines = STRING_LIST_INIT_NODUP;
2793+
struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };
2794+
struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };
27262795
struct branch *b;
27272796
char *author = NULL;
27282797
char *committer = NULL;
2729-
char *sig_alg = NULL;
27302798
char *encoding = NULL;
27312799
struct hash_list *merge_list = NULL;
27322800
unsigned int merge_count;
@@ -2750,13 +2818,23 @@ static void parse_new_commit(const char *arg)
27502818
}
27512819
if (!committer)
27522820
die("Expected committer but didn't get one");
2753-
if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
2754-
sig_alg = xstrdup(v);
2755-
read_next_command();
2756-
parse_data(&sig, 0, NULL);
2821+
2822+
/* Process signatures (up to 2: one "sha1" and one "sha256") */
2823+
while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
2824+
struct signature_data sig = { NULL, NULL, STRBUF_INIT };
2825+
2826+
parse_one_signature(&sig, v);
2827+
2828+
if (!strcmp(sig.hash_algo, "sha1"))
2829+
store_signature(&sig_sha1, &sig, "SHA-1");
2830+
else if (!strcmp(sig.hash_algo, "sha256"))
2831+
store_signature(&sig_sha256, &sig, "SHA-256");
2832+
else
2833+
BUG("parse_one_signature() returned unknown hash algo");
2834+
27572835
read_next_command();
2758-
} else
2759-
strbuf_setlen(&sig, 0);
2836+
}
2837+
27602838
if (skip_prefix(command_buf.buf, "encoding ", &v)) {
27612839
encoding = xstrdup(v);
27622840
read_next_command();
@@ -2830,23 +2908,14 @@ static void parse_new_commit(const char *arg)
28302908
strbuf_addf(&new_data,
28312909
"encoding %s\n",
28322910
encoding);
2833-
if (sig_alg) {
2834-
if (!strcmp(sig_alg, "sha1"))
2835-
strbuf_addstr(&new_data, "gpgsig ");
2836-
else if (!strcmp(sig_alg, "sha256"))
2837-
strbuf_addstr(&new_data, "gpgsig-sha256 ");
2838-
else
2839-
die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
2840-
string_list_split_in_place(&siglines, sig.buf, "\n", -1);
2841-
strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
2842-
strbuf_addch(&new_data, '\n');
2843-
}
2911+
2912+
add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
2913+
add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
2914+
28442915
strbuf_addch(&new_data, '\n');
28452916
strbuf_addbuf(&new_data, &msg);
2846-
string_list_clear(&siglines, 1);
28472917
free(author);
28482918
free(committer);
2849-
free(sig_alg);
28502919
free(encoding);
28512920

28522921
if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))

gpg-interface.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
144144
return NULL;
145145
}
146146

147+
const char *get_signature_format(const char *buf)
148+
{
149+
struct gpg_format *format = get_format_by_sig(buf);
150+
return format ? format->name : "unknown";
151+
}
152+
153+
int valid_signature_format(const char *format)
154+
{
155+
return (!!get_format_by_name(format) ||
156+
!strcmp(format, "unknown"));
157+
}
158+
147159
void signature_check_clear(struct signature_check *sigc)
148160
{
149161
FREE_AND_NULL(sigc->payload);

gpg-interface.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ struct signature_check {
4747

4848
void signature_check_clear(struct signature_check *sigc);
4949

50+
/*
51+
* Return the format of the signature (like "openpgp", "x509", "ssh"
52+
* or "unknown").
53+
*/
54+
const char *get_signature_format(const char *buf);
55+
56+
/*
57+
* Is the signature format valid (like "openpgp", "x509", "ssh" or
58+
* "unknown")
59+
*/
60+
int valid_signature_format(const char *format);
61+
5062
/*
5163
* Look at a GPG signed tag object. If such a signature exists, store it in
5264
* signature and the signed content in payload. Return 1 if a signature was

0 commit comments

Comments
 (0)