{"thread":{"id":"65363","subject":"[PATCH v2 0/5] fast-import: extend signed object handling modes","startedAt":"2026-03-26T19:14:21Z","lastAt":"2026-03-31T23:18:04Z","messageCount":8,"participants":["Justin Tobler","Junio C Hamano"],"isPatch":true,"patchVersion":2,"patchTotal":5},"messages":[{"id":"540122","messageId":"20260326191414.3783974-1-jltobler@gmail.com","threadId":"65363","inReplyTo":null,"subject":"[PATCH v2 0/5] fast-import: extend signed object handling modes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-26T19:14:09Z","receivedAt":"2026-03-26T19:14:21Z","isPatch":true,"body":"Greetings,\n\nThe '--signed-{commits,tags}=<mode>' options for git-fast-import(1)\nallow users to configure how signed objects should be handled at time of\nimport. With c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17) and ee66c793f8 (fast-import: add\nmode to sign commits with invalid signatures, 2026-03-12), the\n'strip-if-invalid' and 'sign-if-invalid' modes were added for the\n'--signed-commits' option only.\n\nThis series extends '--signed-commits' by adding an 'abort-if-invalid'\nmode which aborts the entire import operation when a commit signature\nfails verification. Additionally, the '--signed-tags' option is brought\ninto parity with '--signed-commits' by supporting equivalent,\n'strip-if-invalid', 'sign-if-invalid', and 'abort-if-invalid' modes.\n\nThis series is built on top of 1080981ddb (The 19th batch, 2026-03-23)\nwith ee66c793f8 (fast-import: add mode to sign commits with invalid\nsignatures, 2026-03-12) merged into it.\n\nChanges since V1:\n- Added a prepatory patch which unifies how unsupported signing modes\n  are handled for git-fast-export(1). Now they are treated like any\n  other unknown signing mode. Unsupported signing modes for\n  '--signed-tags' in git-fast-import(1) are left alone because this\n  series progressively adds support for all these currently unsupported\n  modes.\n\nThanks,\n-Justin\n\nJustin Tobler (5):\n  fast-export: check for unsupported signing modes earlier\n  fast-import: add 'abort-if-invalid' mode to '--signed-commits=<mode>'\n  fast-import: add 'strip-if-invalid' mode to '--signed-tags=<mode>'\n  fast-import: add 'sign-if-invalid' mode to '--signed-tags=<mode>'\n  fast-import: add 'abort-if-invalid' mode to '--signed-tags=<mode>'\n\n Documentation/git-fast-import.adoc |   9 ++-\n builtin/fast-export.c              |  15 +---\n builtin/fast-import.c              |  71 ++++++++++++++---\n gpg-interface.c                    |   2 +\n gpg-interface.h                    |   1 +\n t/t9305-fast-import-signatures.sh  |  10 ++-\n t/t9306-fast-import-signed-tags.sh | 118 +++++++++++++++++++++++++++++\n 7 files changed, 197 insertions(+), 29 deletions(-)\n\nRange-diff against v1:\n-:  ---------- > 1:  1dd316e66c fast-export: check for unsupported signing modes earlier\n1:  0e9721fa57 ! 2:  7f34a4ccd5 fast-import: add 'abort-if-invalid' mode to '--signed-commits=<mode>'\n    @@ Documentation/git-fast-import.adoc: already trusted to run their own code.\n      ~~~~~~~~~~~~~~~~~~~~~\n     \n      ## builtin/fast-export.c ##\n    -@@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct rev_info *rev,\n    - \t\t\tdie(_(\"encountered signed commit %s; use \"\n    - \t\t\t      \"--signed-commits=<mode> to handle it\"),\n    - \t\t\t    oid_to_hex(&commit->object.oid));\n    -+\t\tcase SIGN_ABORT_IF_INVALID:\n    -+\t\t\tdie(_(\"'abort-if-invalid' is not a valid mode for \"\n    -+\t\t\t      \"git fast-export with --signed-commits=<mode>\"));\n    - \t\tcase SIGN_STRIP_IF_INVALID:\n    - \t\t\tdie(_(\"'strip-if-invalid' is not a valid mode for \"\n    - \t\t\t      \"git fast-export with --signed-commits=<mode>\"));\n    -@@ builtin/fast-export.c: static void handle_tag(const char *name, struct tag *tag)\n    - \t\t\t\tdie(_(\"encountered signed tag %s; use \"\n    - \t\t\t\t      \"--signed-tags=<mode> to handle it\"),\n    - \t\t\t\t    oid_to_hex(&tag->object.oid));\n    -+\t\t\tcase SIGN_ABORT_IF_INVALID:\n    -+\t\t\t\tdie(_(\"'abort-if-invalid' is not a valid mode for \"\n    -+\t\t\t\t      \"git fast-export with --signed-tags=<mode>\"));\n    - \t\t\tcase SIGN_STRIP_IF_INVALID:\n    - \t\t\t\tdie(_(\"'strip-if-invalid' is not a valid mode for \"\n    - \t\t\t\t      \"git fast-export with --signed-tags=<mode>\"));\n    +@@ builtin/fast-export.c: static int parse_opt_sign_mode(const struct option *opt,\n    + \t\treturn 0;\n    + \n    + \tif (parse_sign_mode(arg, val, NULL) || (*val == SIGN_STRIP_IF_INVALID) ||\n    +-\t    (*val == SIGN_SIGN_IF_INVALID))\n    ++\t    (*val == SIGN_SIGN_IF_INVALID) || (*val == SIGN_ABORT_IF_INVALID))\n    + \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n    + \n    + \treturn 0;\n     \n      ## builtin/fast-import.c ##\n     @@ builtin/fast-import.c: static void handle_signature_if_invalid(struct strbuf *new_data,\n2:  18c145c630 = 3:  adc7289213 fast-import: add 'strip-if-invalid' mode to '--signed-tags=<mode>'\n3:  58a8216447 = 4:  47ff0060a8 fast-import: add 'sign-if-invalid' mode to '--signed-tags=<mode>'\n4:  83476b8971 = 5:  552fea76ca fast-import: add 'abort-if-invalid' mode to '--signed-tags=<mode>'\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"540123","messageId":"20260326191414.3783974-2-jltobler@gmail.com","threadId":"65363","inReplyTo":"20260326191414.3783974-1-jltobler@gmail.com","subject":"[PATCH v2 1/5] fast-export: check for unsupported signing modes earlier","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-26T19:14:10Z","receivedAt":"2026-03-26T19:14:22Z","isPatch":true,"body":"The '--signed-{commits,tags}' options for git-fast-export(1) support\nonly a subset of the modes accepted by git-fast-import(1). Unsupported\nmodes such as 'strip-if-invalid' and 'sign-if-invalid' are accepted\nduring option parsing, but cause the command to die later when a signed\nobject is encountered.\n\nInstead, reject unsupported signing modes immediately after parsing the\noption. This treats them the same as other unknown modes and avoids\ndeferring the error until object processing. This also removes\nduplicated checks in commit/tag handling code.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/fast-export.c | 15 ++-------------\n 1 file changed, 2 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 13621b0d6a..a30fb90b6e 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -64,7 +64,8 @@ static int parse_opt_sign_mode(const struct option *opt,\n \tif (unset)\n \t\treturn 0;\n \n-\tif (parse_sign_mode(arg, val, NULL))\n+\tif (parse_sign_mode(arg, val, NULL) || (*val == SIGN_STRIP_IF_INVALID) ||\n+\t    (*val == SIGN_SIGN_IF_INVALID))\n \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n \n \treturn 0;\n@@ -822,12 +823,6 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t\t\tdie(_(\"encountered signed commit %s; use \"\n \t\t\t      \"--signed-commits=<mode> to handle it\"),\n \t\t\t    oid_to_hex(&commit->object.oid));\n-\t\tcase SIGN_STRIP_IF_INVALID:\n-\t\t\tdie(_(\"'strip-if-invalid' is not a valid mode for \"\n-\t\t\t      \"git fast-export with --signed-commits=<mode>\"));\n-\t\tcase SIGN_SIGN_IF_INVALID:\n-\t\t\tdie(_(\"'sign-if-invalid' is not a valid mode for \"\n-\t\t\t      \"git fast-export with --signed-commits=<mode>\"));\n \t\tdefault:\n \t\t\tBUG(\"invalid signed_commit_mode value %d\", signed_commit_mode);\n \t\t}\n@@ -970,12 +965,6 @@ static void handle_tag(const char *name, struct tag *tag)\n \t\t\t\tdie(_(\"encountered signed tag %s; use \"\n \t\t\t\t      \"--signed-tags=<mode> to handle it\"),\n \t\t\t\t    oid_to_hex(&tag->object.oid));\n-\t\t\tcase SIGN_STRIP_IF_INVALID:\n-\t\t\t\tdie(_(\"'strip-if-invalid' is not a valid mode for \"\n-\t\t\t\t      \"git fast-export with --signed-tags=<mode>\"));\n-\t\t\tcase SIGN_SIGN_IF_INVALID:\n-\t\t\t\tdie(_(\"'sign-if-invalid' is not a valid mode for \"\n-\t\t\t\t      \"git fast-export with --signed-tags=<mode>\"));\n \t\t\tdefault:\n \t\t\t\tBUG(\"invalid signed_commit_mode value %d\", signed_commit_mode);\n \t\t\t}\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"540124","messageId":"20260326191414.3783974-3-jltobler@gmail.com","threadId":"65363","inReplyTo":"20260326191414.3783974-1-jltobler@gmail.com","subject":"[PATCH v2 2/5] fast-import: add 'abort-if-invalid' mode to '--signed-commits=<mode>'","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-26T19:14:11Z","receivedAt":"2026-03-26T19:14:23Z","isPatch":true,"body":"The '--signed-commits=<mode>' option for git-fast-import(1) configures\nhow signed commits are handled when encountered. In cases where an\ninvalid commit signature is encountered, a user may wish to abort the\noperation entirely. Introduce an 'abort-if-invalid' mode to do so.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |  2 ++\n builtin/fast-export.c              |  2 +-\n builtin/fast-import.c              | 10 +++++++++-\n gpg-interface.c                    |  2 ++\n gpg-interface.h                    |  1 +\n t/t9305-fast-import-signatures.sh  | 10 +++++++++-\n 6 files changed, 24 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex b3f42d4637..288f2b2a7e 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -90,6 +90,8 @@ already trusted to run their own code.\n   commit signatures and replaces invalid signatures with newly created ones.\n   Valid signatures are left unchanged. If `<keyid>` is provided, that key is\n   used for signing; otherwise the configured default signing key is used.\n+* `abort-if-invalid` will make this program die when encountering a signed\n+  commit that is unable to be verified.\n \n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex a30fb90b6e..2eb43a28da 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -65,7 +65,7 @@ static int parse_opt_sign_mode(const struct option *opt,\n \t\treturn 0;\n \n \tif (parse_sign_mode(arg, val, NULL) || (*val == SIGN_STRIP_IF_INVALID) ||\n-\t    (*val == SIGN_SIGN_IF_INVALID))\n+\t    (*val == SIGN_SIGN_IF_INVALID) || (*val == SIGN_ABORT_IF_INVALID))\n \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n \n \treturn 0;\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 9fc6c35b74..08ea27242d 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -2892,6 +2892,9 @@ static void handle_signature_if_invalid(struct strbuf *new_data,\n \tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n \n \tif (ret) {\n+\t\tif (mode == SIGN_ABORT_IF_INVALID)\n+\t\t\tdie(_(\"aborting due to invalid signature\"));\n+\n \t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n \n \t\tif (mode == SIGN_SIGN_IF_INVALID) {\n@@ -2983,6 +2986,7 @@ static void parse_new_commit(const char *arg)\n \t\tcase SIGN_VERBATIM:\n \t\tcase SIGN_STRIP_IF_INVALID:\n \t\tcase SIGN_SIGN_IF_INVALID:\n+\t\tcase SIGN_ABORT_IF_INVALID:\n \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n@@ -3068,7 +3072,8 @@ static void parse_new_commit(const char *arg)\n \t\t\tencoding);\n \n \tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n-\t     signed_commit_mode == SIGN_SIGN_IF_INVALID) &&\n+\t     signed_commit_mode == SIGN_SIGN_IF_INVALID ||\n+\t     signed_commit_mode == SIGN_ABORT_IF_INVALID) &&\n \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n \t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n \t\t\t\t\t    &msg, signed_commit_mode);\n@@ -3115,6 +3120,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\n \tcase SIGN_ABORT:\n \t\tdie(_(\"encountered signed tag; use \"\n \t\t      \"--signed-tags=<mode> to handle it\"));\n+\tcase SIGN_ABORT_IF_INVALID:\n+\t\tdie(_(\"'abort-if-invalid' is not a valid mode for \"\n+\t\t      \"git fast-import with --signed-tags=<mode>\"));\n \tcase SIGN_STRIP_IF_INVALID:\n \t\tdie(_(\"'strip-if-invalid' is not a valid mode for \"\n \t\t      \"git fast-import with --signed-tags=<mode>\"));\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex d517425034..dafd5371fa 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1164,6 +1164,8 @@ int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)\n \t\t*mode = SIGN_WARN_STRIP;\n \t} else if (!strcmp(arg, \"strip\")) {\n \t\t*mode = SIGN_STRIP;\n+\t} else if (!strcmp(arg, \"abort-if-invalid\")) {\n+\t\t*mode = SIGN_ABORT_IF_INVALID;\n \t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n \t\t*mode = SIGN_STRIP_IF_INVALID;\n \t} else if (!strcmp(arg, \"sign-if-invalid\")) {\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex a365586ce1..3d95f5ec14 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -115,6 +115,7 @@ void print_signature_buffer(const struct signature_check *sigc,\n /* Modes for --signed-tags=<mode> and --signed-commits=<mode> options. */\n enum sign_mode {\n \tSIGN_ABORT,\n+\tSIGN_ABORT_IF_INVALID,\n \tSIGN_WARN_VERBATIM,\n \tSIGN_VERBATIM,\n \tSIGN_WARN_STRIP,\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nindex 18707b3f6c..5667693afd 100755\n--- a/t/t9305-fast-import-signatures.sh\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -103,7 +103,7 @@ test_expect_success RUST,GPG 'strip both OpenPGP signatures with --signed-commit\n \ttest_line_count = 2 out\n '\n \n-for mode in strip-if-invalid sign-if-invalid\n+for mode in strip-if-invalid sign-if-invalid abort-if-invalid\n do\n \ttest_expect_success GPG \"import commit with no signature with --signed-commits=$mode\" '\n \t\tgit fast-export main >output &&\n@@ -135,6 +135,14 @@ do\n \t\t# corresponding `data <length>` command would have to be changed too.\n \t\tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n \n+\t\tif test \"$mode\" = abort-if-invalid\n+\t\tthen\n+\t\t\ttest_must_fail git -C new fast-import --quiet \\\n+\t\t\t\t--signed-commits=$mode <modified >log 2>&1 &&\n+\t\t\ttest_grep \"aborting due to invalid signature\" log &&\n+\t\t\treturn 0\n+\t\tfi &&\n+\n \t\tgit -C new fast-import --quiet --signed-commits=$mode <modified >log 2>&1 &&\n \n \t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"540125","messageId":"20260326191414.3783974-4-jltobler@gmail.com","threadId":"65363","inReplyTo":"20260326191414.3783974-1-jltobler@gmail.com","subject":"[PATCH v2 3/5] fast-import: add 'strip-if-invalid' mode to '--signed-tags=<mode>'","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-26T19:14:12Z","receivedAt":"2026-03-26T19:14:24Z","isPatch":true,"body":"With c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17), git-fast-import(1) learned to\nverify commit signatures during import and strip signatures that fail\nverification. Extend the same behavior to signed tag objects by\nintroducing a 'strip-if-invalid' mode for the '--signed-tags' option.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |  7 ++-\n builtin/fast-import.c              | 40 +++++++++++++---\n t/t9306-fast-import-signed-tags.sh | 73 ++++++++++++++++++++++++++++++\n 3 files changed, 110 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 288f2b2a7e..d68bc52b7e 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -66,11 +66,10 @@ fast-import stream! This option is enabled automatically for\n remote-helpers that use the `import` capability, as they are\n already trusted to run their own code.\n \n-`--signed-tags=(verbatim|warn-verbatim|warn-strip|strip|abort)`::\n+`--signed-tags=<mode>`::\n \tSpecify how to handle signed tags. Behaves in the same way as\n-\tthe `--signed-commits=<mode>` below, except that the\n-\t`strip-if-invalid` mode is not yet supported. Like for signed\n-\tcommits, the default mode is `verbatim`.\n+\tthe `--signed-commits=<mode>` below. Like for signed commits,\n+\tthe default mode is `verbatim`.\n \n `--signed-commits=<mode>`::\n \tSpecify how to handle signed commits. The following <mode>s\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 08ea27242d..5e89829aea 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3089,7 +3089,34 @@ static void parse_new_commit(const char *arg)\n \tb->last_commit = object_count_by_type[OBJ_COMMIT];\n }\n \n-static void handle_tag_signature(struct strbuf *msg, const char *name)\n+static void handle_tag_signature_if_invalid(struct strbuf *buf,\n+\t\t\t\t\t    struct strbuf *msg,\n+\t\t\t\t\t    size_t sig_offset)\n+{\n+\tstruct strbuf signature = STRBUF_INIT;\n+\tstruct strbuf payload = STRBUF_INIT;\n+\tstruct signature_check sigc = { 0 };\n+\n+\tstrbuf_addbuf(&payload, buf);\n+\tstrbuf_addch(&payload, '\\n');\n+\tstrbuf_add(&payload, msg->buf, sig_offset);\n+\tstrbuf_add(&signature, msg->buf + sig_offset, msg->len - sig_offset);\n+\n+\tsigc.payload_type = SIGNATURE_PAYLOAD_TAG;\n+\tsigc.payload = strbuf_detach(&payload, &sigc.payload_len);\n+\n+\tif (!check_signature(&sigc, signature.buf, signature.len))\n+\t\tgoto out;\n+\n+\tstrbuf_setlen(msg, sig_offset);\n+\n+out:\n+\tsignature_check_clear(&sigc);\n+\tstrbuf_release(&signature);\n+\tstrbuf_release(&payload);\n+}\n+\n+static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const char *name)\n {\n \tsize_t sig_offset = parse_signed_buffer(msg->buf, msg->len);\n \n@@ -3115,6 +3142,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\n \t\t/* Truncate the buffer to remove the signature */\n \t\tstrbuf_setlen(msg, sig_offset);\n \t\tbreak;\n+\tcase SIGN_STRIP_IF_INVALID:\n+\t\thandle_tag_signature_if_invalid(buf, msg, sig_offset);\n+\t\tbreak;\n \n \t/* Third, aborting modes */\n \tcase SIGN_ABORT:\n@@ -3123,9 +3153,6 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\n \tcase SIGN_ABORT_IF_INVALID:\n \t\tdie(_(\"'abort-if-invalid' is not a valid mode for \"\n \t\t      \"git fast-import with --signed-tags=<mode>\"));\n-\tcase SIGN_STRIP_IF_INVALID:\n-\t\tdie(_(\"'strip-if-invalid' is not a valid mode for \"\n-\t\t      \"git fast-import with --signed-tags=<mode>\"));\n \tcase SIGN_SIGN_IF_INVALID:\n \t\tdie(_(\"'sign-if-invalid' is not a valid mode for \"\n \t\t      \"git fast-import with --signed-tags=<mode>\"));\n@@ -3198,8 +3225,6 @@ static void parse_new_tag(const char *arg)\n \t/* tag payload/message */\n \tparse_data(&msg, 0, NULL);\n \n-\thandle_tag_signature(&msg, t->name);\n-\n \t/* build the tag object */\n \tstrbuf_reset(&new_data);\n \n@@ -3211,6 +3236,9 @@ static void parse_new_tag(const char *arg)\n \tif (tagger)\n \t\tstrbuf_addf(&new_data,\n \t\t\t    \"tagger %s\\n\", tagger);\n+\n+\thandle_tag_signature(&new_data, &msg, t->name);\n+\n \tstrbuf_addch(&new_data, '\\n');\n \tstrbuf_addbuf(&new_data, &msg);\n \tfree(tagger);\ndiff --git a/t/t9306-fast-import-signed-tags.sh b/t/t9306-fast-import-signed-tags.sh\nindex 363619e7d1..fd43b0b52a 100755\n--- a/t/t9306-fast-import-signed-tags.sh\n+++ b/t/t9306-fast-import-signed-tags.sh\n@@ -77,4 +77,77 @@ test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=strip' '\n \ttest_grep ! \"SSH SIGNATURE\" out\n '\n \n+for mode in strip-if-invalid\n+do\n+\ttest_expect_success GPG \"import tag with no signature with --signed-tags=$mode\" '\n+\t\ttest_when_finished rm -rf import &&\n+\t\tgit init import &&\n+\n+\t\tgit fast-export --signed-tags=verbatim >output &&\n+\t\tgit -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"keep valid OpenPGP signature with --signed-tags=$mode\" '\n+\t\ttest_when_finished rm -rf import &&\n+\t\tgit init import &&\n+\n+\t\tgit fast-export --signed-tags=verbatim openpgp-signed >output &&\n+\t\tgit -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C import rev-parse --verify refs/tags/openpgp-signed) &&\n+\t\ttest $OPENPGP_SIGNED = $IMPORTED &&\n+\t\tgit -C import cat-file tag \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^-----BEGIN PGP SIGNATURE-----\" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"handle signature invalidated by message change with --signed-tags=$mode\" '\n+\t\ttest_when_finished rm -rf import &&\n+\t\tgit init import &&\n+\n+\t\tgit fast-export --signed-tags=verbatim openpgp-signed >output &&\n+\n+\t\t# Change the tag message, which invalidates the signature. The tag\n+\t\t# message length should not change though, otherwise the corresponding\n+\t\t# `data <length>` command would have to be changed too.\n+\t\tsed \"s/OpenPGP signed tag/OpenPGP forged tag/\" output >modified &&\n+\n+\t\tgit -C import fast-import --quiet --signed-tags=$mode <modified >log 2>&1 &&\n+\n+\t\tIMPORTED=$(git -C import rev-parse --verify refs/tags/openpgp-signed) &&\n+\t\ttest $OPENPGP_SIGNED != $IMPORTED &&\n+\t\tgit -C import cat-file tag \"$IMPORTED\" >actual &&\n+\t\ttest_grep ! -E \"^-----BEGIN PGP SIGNATURE-----\" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSM \"keep valid X.509 signature with --signed-tags=$mode\" '\n+\t\ttest_when_finished rm -rf import &&\n+\t\tgit init import &&\n+\n+\t\tgit fast-export --signed-tags=verbatim x509-signed >output &&\n+\t\tgit -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C import rev-parse --verify refs/tags/x509-signed) &&\n+\t\ttest $X509_SIGNED = $IMPORTED &&\n+\t\tgit -C import cat-file tag x509-signed >actual &&\n+\t\ttest_grep -E \"^-----BEGIN SIGNED MESSAGE-----\" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSSH \"keep valid SSH signature with --signed-tags=$mode\" '\n+\t\ttest_when_finished rm -rf import &&\n+\t\tgit init import &&\n+\n+\t\ttest_config -C import gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t\tgit fast-export --signed-tags=verbatim ssh-signed >output &&\n+\t\tgit -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C import rev-parse --verify refs/tags/ssh-signed) &&\n+\t\ttest $SSH_SIGNED = $IMPORTED &&\n+\t\tgit -C import cat-file tag ssh-signed >actual &&\n+\t\ttest_grep -E \"^-----BEGIN SSH SIGNATURE-----\" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+done\n+\n test_done\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"540126","messageId":"20260326191414.3783974-5-jltobler@gmail.com","threadId":"65363","inReplyTo":"20260326191414.3783974-1-jltobler@gmail.com","subject":"[PATCH v2 4/5] fast-import: add 'sign-if-invalid' mode to '--signed-tags=<mode>'","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-26T19:14:13Z","receivedAt":"2026-03-26T19:14:25Z","isPatch":true,"body":"With ee66c793f8 (fast-import: add mode to sign commits with invalid\nsignatures, 2026-03-12), git-fast-import(1) learned to verify commit\nsignatures during import and replace signatures that fail verification\nwith a newly generated one. Extend the same behavior to signed tag\nobjects by introducing a 'sign-if-invalid' mode for the '--signed-tags'\noption.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/fast-import.c              | 20 ++++++++++++---\n t/t9306-fast-import-signed-tags.sh | 41 ++++++++++++++++++++++++++++--\n 2 files changed, 55 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 5e89829aea..783e0e7ab4 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -191,6 +191,7 @@ static const char *global_prefix;\n static enum sign_mode signed_tag_mode = SIGN_VERBATIM;\n static enum sign_mode signed_commit_mode = SIGN_VERBATIM;\n static const char *signed_commit_keyid;\n+static const char *signed_tag_keyid;\n \n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n@@ -3110,6 +3111,19 @@ static void handle_tag_signature_if_invalid(struct strbuf *buf,\n \n \tstrbuf_setlen(msg, sig_offset);\n \n+\tif (signed_tag_mode == SIGN_SIGN_IF_INVALID) {\n+\t\tstrbuf_attach(&payload, sigc.payload, sigc.payload_len,\n+\t\t\t      sigc.payload_len + 1);\n+\t\tsigc.payload = NULL;\n+\t\tstrbuf_reset(&signature);\n+\n+\t\tif (sign_buffer(&payload, &signature, signed_tag_keyid,\n+\t\t\t\tSIGN_BUFFER_USE_DEFAULT_KEY))\n+\t\t\tdie(_(\"failed to sign tag object\"));\n+\n+\t\tstrbuf_addbuf(msg, &signature);\n+\t}\n+\n out:\n \tsignature_check_clear(&sigc);\n \tstrbuf_release(&signature);\n@@ -3142,6 +3156,7 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c\n \t\t/* Truncate the buffer to remove the signature */\n \t\tstrbuf_setlen(msg, sig_offset);\n \t\tbreak;\n+\tcase SIGN_SIGN_IF_INVALID:\n \tcase SIGN_STRIP_IF_INVALID:\n \t\thandle_tag_signature_if_invalid(buf, msg, sig_offset);\n \t\tbreak;\n@@ -3153,9 +3168,6 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c\n \tcase SIGN_ABORT_IF_INVALID:\n \t\tdie(_(\"'abort-if-invalid' is not a valid mode for \"\n \t\t      \"git fast-import with --signed-tags=<mode>\"));\n-\tcase SIGN_SIGN_IF_INVALID:\n-\t\tdie(_(\"'sign-if-invalid' is not a valid mode for \"\n-\t\t      \"git fast-import with --signed-tags=<mode>\"));\n \tdefault:\n \t\tBUG(\"invalid signed_tag_mode value %d from tag '%s'\",\n \t\t    signed_tag_mode, name);\n@@ -3749,7 +3761,7 @@ static int parse_one_option(const char *option)\n \t\tif (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid))\n \t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n \t} else if (skip_prefix(option, \"signed-tags=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_tag_mode, NULL))\n+\t\tif (parse_sign_mode(option, &signed_tag_mode, &signed_tag_keyid))\n \t\t\tusagef(_(\"unknown --signed-tags mode '%s'\"), option);\n \t} else if (!strcmp(option, \"quiet\")) {\n \t\tshow_stats = 0;\ndiff --git a/t/t9306-fast-import-signed-tags.sh b/t/t9306-fast-import-signed-tags.sh\nindex fd43b0b52a..bb4c8008ef 100755\n--- a/t/t9306-fast-import-signed-tags.sh\n+++ b/t/t9306-fast-import-signed-tags.sh\n@@ -77,7 +77,7 @@ test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=strip' '\n \ttest_grep ! \"SSH SIGNATURE\" out\n '\n \n-for mode in strip-if-invalid\n+for mode in strip-if-invalid sign-if-invalid\n do\n \ttest_expect_success GPG \"import tag with no signature with --signed-tags=$mode\" '\n \t\ttest_when_finished rm -rf import &&\n@@ -117,7 +117,15 @@ do\n \t\tIMPORTED=$(git -C import rev-parse --verify refs/tags/openpgp-signed) &&\n \t\ttest $OPENPGP_SIGNED != $IMPORTED &&\n \t\tgit -C import cat-file tag \"$IMPORTED\" >actual &&\n-\t\ttest_grep ! -E \"^-----BEGIN PGP SIGNATURE-----\" actual &&\n+\n+\t\tif test \"$mode\" = strip-if-invalid\n+\t\tthen\n+\t\t\ttest_grep ! -E \"^-----BEGIN PGP SIGNATURE-----\" actual\n+\t\telse\n+\t\t\ttest_grep -E \"^-----BEGIN PGP SIGNATURE-----\" actual &&\n+\t\t\tgit -C import verify-tag \"$IMPORTED\"\n+\t\tfi &&\n+\n \t\ttest_must_be_empty log\n \t'\n \n@@ -150,4 +158,33 @@ do\n \t'\n done\n \n+test_expect_success GPGSSH 'sign invalid tag with explicit keyid' '\n+\ttest_when_finished rm -rf import &&\n+\tgit init import &&\n+\n+\tgit fast-export --signed-tags=verbatim ssh-signed >output &&\n+\n+\t# Change the tag message, which invalidates the signature. The tag\n+\t# message length should not change though, otherwise the corresponding\n+\t# `data <length>` command would have to be changed too.\n+\tsed \"s/SSH signed tag/SSH forged tag/\" output >modified &&\n+\n+\t# Configure the target repository with an invalid default signing key.\n+\ttest_config -C import user.signingkey \"not-a-real-key-id\" &&\n+\ttest_config -C import gpg.format ssh &&\n+\ttest_config -C import gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git -C import fast-import --quiet \\\n+\t\t--signed-tags=sign-if-invalid <modified >/dev/null 2>&1 &&\n+\n+\t# Import using explicitly provided signing key.\n+\tgit -C import fast-import --quiet \\\n+\t\t--signed-tags=sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n+\n+\tIMPORTED=$(git -C import rev-parse --verify refs/tags/ssh-signed) &&\n+\ttest $SSH_SIGNED != $IMPORTED &&\n+\tgit -C import cat-file tag \"$IMPORTED\" >actual &&\n+\ttest_grep -E \"^-----BEGIN SSH SIGNATURE-----\" actual &&\n+\tgit -C import verify-tag \"$IMPORTED\"\n+'\n+\n test_done\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"540128","messageId":"20260326191414.3783974-6-jltobler@gmail.com","threadId":"65363","inReplyTo":"20260326191414.3783974-1-jltobler@gmail.com","subject":"[PATCH v2 5/5] fast-import: add 'abort-if-invalid' mode to '--signed-tags=<mode>'","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-26T19:14:14Z","receivedAt":"2026-03-26T19:14:26Z","isPatch":true,"body":"In git-fast-import(1), the 'abort-if-invalid' mode for the\n'--signed-commits' option verifies commit signatures during import and\naborts the entire operation when verification fails. Extend the same\nbehavior to signed tag objects by introducing an 'abort-if-invalid' mode\nfor the '--signed-tags' option.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/fast-import.c              |  7 ++++---\n t/t9306-fast-import-signed-tags.sh | 10 +++++++++-\n 2 files changed, 13 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 783e0e7ab4..cd1181023d 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -3109,6 +3109,9 @@ static void handle_tag_signature_if_invalid(struct strbuf *buf,\n \tif (!check_signature(&sigc, signature.buf, signature.len))\n \t\tgoto out;\n \n+\tif (signed_tag_mode == SIGN_ABORT_IF_INVALID)\n+\t\tdie(_(\"aborting due to invalid signature\"));\n+\n \tstrbuf_setlen(msg, sig_offset);\n \n \tif (signed_tag_mode == SIGN_SIGN_IF_INVALID) {\n@@ -3156,6 +3159,7 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c\n \t\t/* Truncate the buffer to remove the signature */\n \t\tstrbuf_setlen(msg, sig_offset);\n \t\tbreak;\n+\tcase SIGN_ABORT_IF_INVALID:\n \tcase SIGN_SIGN_IF_INVALID:\n \tcase SIGN_STRIP_IF_INVALID:\n \t\thandle_tag_signature_if_invalid(buf, msg, sig_offset);\n@@ -3165,9 +3169,6 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c\n \tcase SIGN_ABORT:\n \t\tdie(_(\"encountered signed tag; use \"\n \t\t      \"--signed-tags=<mode> to handle it\"));\n-\tcase SIGN_ABORT_IF_INVALID:\n-\t\tdie(_(\"'abort-if-invalid' is not a valid mode for \"\n-\t\t      \"git fast-import with --signed-tags=<mode>\"));\n \tdefault:\n \t\tBUG(\"invalid signed_tag_mode value %d from tag '%s'\",\n \t\t    signed_tag_mode, name);\ndiff --git a/t/t9306-fast-import-signed-tags.sh b/t/t9306-fast-import-signed-tags.sh\nindex bb4c8008ef..ec2b241cdb 100755\n--- a/t/t9306-fast-import-signed-tags.sh\n+++ b/t/t9306-fast-import-signed-tags.sh\n@@ -77,7 +77,7 @@ test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=strip' '\n \ttest_grep ! \"SSH SIGNATURE\" out\n '\n \n-for mode in strip-if-invalid sign-if-invalid\n+for mode in strip-if-invalid sign-if-invalid abort-if-invalid\n do\n \ttest_expect_success GPG \"import tag with no signature with --signed-tags=$mode\" '\n \t\ttest_when_finished rm -rf import &&\n@@ -112,6 +112,14 @@ do\n \t\t# `data <length>` command would have to be changed too.\n \t\tsed \"s/OpenPGP signed tag/OpenPGP forged tag/\" output >modified &&\n \n+\t\tif test \"$mode\" = abort-if-invalid\n+\t\tthen\n+\t\t\ttest_must_fail git -C import fast-import --quiet \\\n+\t\t\t\t--signed-tags=$mode <modified >log 2>&1 &&\n+\t\t\ttest_grep \"aborting due to invalid signature\" log &&\n+\t\t\treturn 0\n+\t\tfi &&\n+\n \t\tgit -C import fast-import --quiet --signed-tags=$mode <modified >log 2>&1 &&\n \n \t\tIMPORTED=$(git -C import rev-parse --verify refs/tags/openpgp-signed) &&\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"540571","messageId":"xmqqtstvwsxe.fsf@gitster.g","threadId":"65363","inReplyTo":"20260326191414.3783974-1-jltobler@gmail.com","subject":"Re: [PATCH v2 0/5] fast-import: extend signed object handling modes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-31T22:11:25Z","receivedAt":"2026-03-31T22:11:27Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> Changes since V1:\n> - Added a prepatory patch which unifies how unsupported signing modes\n>   are handled for git-fast-export(1). Now they are treated like any\n>   other unknown signing mode. Unsupported signing modes for\n>   '--signed-tags' in git-fast-import(1) are left alone because this\n>   series progressively adds support for all these currently unsupported\n>   modes.\n\nEven though you have this inter-iteration change log (which is very\ngood), as the cover letter is not sent as a reply to the cover\nletter of the previous iteration, the mailing list archive\n\n  https://lore.kernel.org/git/20260326191414.3783974-1-jltobler@gmail.com/\n\ndoes not help us navigate to the previous iteration\n\n  https://lore.kernel.org/git/20260324215513.764739-1-jltobler@gmail.com/\n\nvery easily (I found the above URL by cheating---I looked at notes/amlog\nfor a commit from previous iteration jt/fast-import-signed-modes@{1}---but\nthat is not for everybody).\n\nLooks like I was the only one who was interested enough to comment\non the patches over these two iterations, which is a bit sad, but I\nthink the patches are fairly cleanly done and are ready for 'next'.\n\nLet me mark the topic as such in the \"What's cooking\" report.\n\nThanks.\n"},{"id":"540582","messageId":"acxVmFy9gYITrgcl@denethor","threadId":"65363","inReplyTo":"xmqqtstvwsxe.fsf@gitster.g","subject":"Re: [PATCH v2 0/5] fast-import: extend signed object handling modes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-31T23:18:02Z","receivedAt":"2026-03-31T23:18:04Z","isPatch":true,"body":"On 26/03/31 03:11PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > Changes since V1:\n> > - Added a prepatory patch which unifies how unsupported signing modes\n> >   are handled for git-fast-export(1). Now they are treated like any\n> >   other unknown signing mode. Unsupported signing modes for\n> >   '--signed-tags' in git-fast-import(1) are left alone because this\n> >   series progressively adds support for all these currently unsupported\n> >   modes.\n> \n> Even though you have this inter-iteration change log (which is very\n> good), as the cover letter is not sent as a reply to the cover\n> letter of the previous iteration, the mailing list archive\n> \n>   https://lore.kernel.org/git/20260326191414.3783974-1-jltobler@gmail.com/\n> \n> does not help us navigate to the previous iteration\n> \n>   https://lore.kernel.org/git/20260324215513.764739-1-jltobler@gmail.com/\n> \n> very easily (I found the above URL by cheating---I looked at notes/amlog\n> for a commit from previous iteration jt/fast-import-signed-modes@{1}---but\n> that is not for everybody).\n\nApologies, I should have payed more attention when sending. Thanks for\nlinking the previous iteration.\n\n> Looks like I was the only one who was interested enough to comment\n> on the patches over these two iterations, which is a bit sad, but I\n> think the patches are fairly cleanly done and are ready for 'next'.\n> \n> Let me mark the topic as such in the \"What's cooking\" report.\n\nThanks,\n-Justin\n"}]}