Skip to content

Commit 2469009

Browse files
committed
fast-export: improve commit signature export
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 so that: - both one signature on the SHA-1 object and one on the SHA-256 object can be exported, - if there is more than one signature on the SHA-1 object or on the SHA-256 object, a warning is emitted, - 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. Note that it could be even better to export 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]>
1 parent 9edff09 commit 2469009

File tree

4 files changed

+84
-12
lines changed

4 files changed

+84
-12
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+
Currently for a given commit, at most one signature for the SHA-1
66+
object and one signature for the SHA-256 object are exported, each
67+
with their respective <git-hash-algo> identifier. A warning is
68+
emitted for each additional signature found.
69+
+
5370
NOTE: This is highly experimental and the format of the data stream may
5471
change in the future without compatibility guarantees.
5572

builtin/fast-export.c

Lines changed: 55 additions & 12 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,30 @@ 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",
662+
object_hash,
663+
get_signature_format(signature),
664+
(unsigned)strlen(signature),
665+
signature);
666+
}
667+
668+
static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)
669+
{
670+
const char *header = is_sha1 ? "gpgsig" : "gpgsig-sha256";
671+
const char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);
672+
if (extra_sig) {
673+
const char *hash = is_sha1 ? "SHA-1" : "SHA-256";
674+
warning("more than one %s signature found on commit %s, using only the first one",
675+
hash, oid_to_hex(&commit->object.oid));
676+
free((char *)extra_sig);
677+
}
678+
}
679+
655680
static void handle_commit(struct commit *commit, struct rev_info *rev,
656681
struct string_list *paths_of_changed_objects)
657682
{
@@ -660,7 +685,8 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
660685
const char *author, *author_end, *committer, *committer_end;
661686
const char *encoding = NULL;
662687
size_t encoding_len;
663-
const char *signature_alg = NULL, *signature = NULL;
688+
const char *sig_sha1 = NULL;
689+
const char *sig_sha256 = NULL;
664690
const char *message;
665691
char *reencoded = NULL;
666692
struct commit_list *p;
@@ -700,10 +726,28 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
700726
}
701727

702728
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";
729+
const char *sig_cursor = commit_buffer_cursor;
730+
const char *after_sha1 = commit_buffer_cursor;
731+
const char *after_sha256 = commit_buffer_cursor;
732+
733+
/*
734+
* Find the first signature for each hash algorithm.
735+
* The searches must start from the same position.
736+
*/
737+
sig_sha1 = find_commit_multiline_header(sig_cursor + 1,
738+
"gpgsig",
739+
&after_sha1);
740+
sig_sha256 = find_commit_multiline_header(sig_cursor + 1,
741+
"gpgsig-sha256",
742+
&after_sha256);
743+
744+
/* Warn on any additional signatures, as they will be ignored. */
745+
if (sig_sha1)
746+
warn_on_extra_sig(&after_sha1, commit, 1);
747+
if (sig_sha256)
748+
warn_on_extra_sig(&after_sha256, commit, 0);
749+
750+
commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
707751
}
708752

709753
message = strstr(commit_buffer_cursor, "\n\n");
@@ -769,7 +813,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
769813
printf("%.*s\n%.*s\n",
770814
(int)(author_end - author), author,
771815
(int)(committer_end - committer), committer);
772-
if (signature) {
816+
if (sig_sha1 || sig_sha256) {
773817
switch (signed_commit_mode) {
774818
case SIGN_ABORT:
775819
die("encountered signed commit %s; use "
@@ -780,19 +824,18 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
780824
oid_to_hex(&commit->object.oid));
781825
/* fallthru */
782826
case SIGN_VERBATIM:
783-
printf("gpgsig %s\ndata %u\n%s",
784-
signature_alg,
785-
(unsigned)strlen(signature),
786-
signature);
827+
print_signature(sig_sha1, "sha1");
828+
print_signature(sig_sha256, "sha256");
787829
break;
788830
case SIGN_WARN_STRIP:
789-
warning("stripping signature from commit %s",
831+
warning("stripping signature(s) from commit %s",
790832
oid_to_hex(&commit->object.oid));
791833
/* fallthru */
792834
case SIGN_STRIP:
793835
break;
794836
}
795-
free((char *)signature);
837+
free((char *)sig_sha1);
838+
free((char *)sig_sha256);
796839
}
797840
if (!reencoded && encoding)
798841
printf("encoding %.*s\n", (int)encoding_len, encoding);

gpg-interface.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ 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+
147153
void signature_check_clear(struct signature_check *sigc)
148154
{
149155
FREE_AND_NULL(sigc->payload);

gpg-interface.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ 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+
5056
/*
5157
* Look at a GPG signed tag object. If such a signature exists, store it in
5258
* signature and the signed content in payload. Return 1 if a signature was

0 commit comments

Comments
 (0)