{"thread":{"id":"65062","subject":"[PATCH 0/2] fast-import: add mode to re-sign invalid commit signatures","startedAt":"2026-02-23T19:42:20Z","lastAt":"2026-03-13T06:32:04Z","messageCount":60,"participants":["Justin Tobler","Patrick Steinhardt","Christian Couder","brian m. carlson","Junio C Hamano","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"536871","messageId":"20260223194146.3476768-1-jltobler@gmail.com","threadId":"65062","inReplyTo":null,"subject":"[PATCH 0/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-02-23T19:41:44Z","receivedAt":"2026-02-23T19:42:20Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nWith c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17), it became possible to remove\ninvalid signatures from commits via git-fast-import(1) while maintaining\nvalid commits. Building upon this functionality, a user may want to\nre-sign these invalid commit signatures. This series introduces the\n`re-sign-if-invalid` mode to do so accordingly.\n\nThe newly added mode in this series currently ignores\n`extensions.compatObjectFormat` when generating the new signatures. From\nmy understanding, to generate the compatability structure would also\nrequire us to reconstruct the compatability object for the object being\nsigned. I think this would be possible to do, but would require getting\nthe mapped OIDs for the commit parents and tree. I'm not competely sure\nof a good way to go about this yet though. I'm also not completely\ncertain if this is something that should be adressed as part of this\nseries, or could be done later down the road. So for now I've opted to\ndelay its implementation. I'm open going down the other route if that is\npreferred though.\n\nThe first commit is a simple cleanup for something I noticed while\nreading though commit signing code. The second commit actually\nintroduces the new `--signed-commits` mode.\n\nThanks,\n-Justin\n\nJustin Tobler (2):\n  commit: remove unused forward declaration\n  fast-import: add mode to re-sign invalid commit signatures\n\n Documentation/git-fast-import.adoc |   3 +\n builtin/fast-export.c              |   6 ++\n builtin/fast-import.c              |  43 +++++++--\n commit.h                           |   2 -\n gpg-interface.c                    |   2 +\n gpg-interface.h                    |   1 +\n t/t9305-fast-import-signatures.sh  | 142 +++++++++++++++--------------\n 7 files changed, 125 insertions(+), 74 deletions(-)\n\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\n-- \n2.53.0\n\n"},{"id":"536872","messageId":"20260223194146.3476768-2-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260223194146.3476768-1-jltobler@gmail.com","subject":"[PATCH 1/2] commit: remove unused forward declaration","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-02-23T19:41:45Z","receivedAt":"2026-02-23T19:42:20Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n`sign_with_header()` was removed, but its forward declaration in\n\"commit.h\" was left. Remove the unused declaration.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.h | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/commit.h b/commit.h\nindex 1635de418b..f0c38cb444 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -390,8 +390,6 @@ LAST_ARG_MUST_BE_NULL\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook, const char *name, ...);\n \n-/* Sign a commit or tag buffer, storing the result in a header. */\n-int sign_with_header(struct strbuf *buf, const char *keyid);\n /* Parse the signature out of a header. */\n int parse_buffer_signed_by_header(const char *buffer,\n \t\t\t\t  unsigned long size,\n-- \n2.53.0\n\n"},{"id":"536873","messageId":"20260223194146.3476768-3-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260223194146.3476768-1-jltobler@gmail.com","subject":"[PATCH 2/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-02-23T19:41:46Z","receivedAt":"2026-02-23T19:42:21Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"With git-fast-import(1), handling of signed commits is controlled via\nthe `--signed-commits=<mode>` option. When an invalid signature is\nencountered, a user may want the option to re-sign the commit as opposed\nto just stripping the signature. To faciliate this, introduce a\n\"re-sign-if-invalid\" mode for the `--signed-commits` option.\n\nNote that commits are re-signed using only the repository object format\nhash algorithm. If a commit has an additional signature due to the\n`compatObjectFormat` repository extension being set, the other signature\nis stripped.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |   3 +\n builtin/fast-export.c              |   6 ++\n builtin/fast-import.c              |  43 +++++++--\n gpg-interface.c                    |   2 +\n gpg-interface.h                    |   1 +\n t/t9305-fast-import-signatures.sh  | 142 +++++++++++++++--------------\n 6 files changed, 125 insertions(+), 72 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 479c4081da..b902a6e2b0 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -86,6 +86,9 @@ already trusted to run their own code.\n * `strip-if-invalid` will check signatures and, if they are invalid,\n   will strip them and display a warning. The validation is performed\n   in the same way as linkgit:git-verify-commit[1] does it.\n+* `re-sign-if-invalid` is the same as `strip-if-invalid`, but additionally the\n+  commits with invalid signatures are signed again, so that old invalid\n+  signatures are replaced with new valid ones.\n \n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 0c5d2386d8..76fad1dec5 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\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_RESIGN_IF_INVALID:\n+\t\t\tdie(_(\"'re-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,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)\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_RESIGN_IF_INVALID:\n+\t\t\t\tdie(_(\"'re-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}\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b8a7757cfd..e34a373d2f 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -2836,10 +2836,11 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n \tstrbuf_addbuf(new_data, msg);\n }\n \n-static void handle_strip_if_invalid(struct strbuf *new_data,\n-\t\t\t\t    struct signature_data *sig_sha1,\n-\t\t\t\t    struct signature_data *sig_sha256,\n-\t\t\t\t    struct strbuf *msg)\n+static void handle_invalid_signature(struct strbuf *new_data,\n+\t\t\t\t     struct signature_data *sig_sha1,\n+\t\t\t\t     struct signature_data *sig_sha256,\n+\t\t\t\t     struct strbuf *msg,\n+\t\t\t\t     enum sign_mode mode)\n {\n \tstruct strbuf tmp_buf = STRBUF_INIT;\n \tstruct signature_check signature_check = { 0 };\n@@ -2866,6 +2867,30 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n \t\t\t\t  \"  allegedly by %s\"), signer);\n \n+\t\tif (mode == SIGN_RESIGN_IF_INVALID) {\n+\t\t\tstruct strbuf signature = STRBUF_INIT;\n+\t\t\tstruct strbuf payload = STRBUF_INIT;\n+\t\t\tchar *key = get_signing_key();\n+\n+\t\t\t/*\n+\t\t\t * Commits are resigned using the repository object\n+\t\t\t * format hash algorithm only. Consequently if\n+\t\t\t * extensions.compatObjectFormat is set, the\n+\t\t\t * compatability hash is not currently used to\n+\t\t\t * additionally sign the commit. If the commit payload\n+\t\t\t * were reconstructed in the compatability format, it\n+\t\t\t * would be possible to generate the other signature\n+\t\t\t * accordingly though.\n+\t\t\t */\n+\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n+\t\t\tsign_buffer(&payload, &signature, key);\n+\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n+\n+\t\t\tstrbuf_release(&signature);\n+\t\t\tstrbuf_release(&payload);\n+\t\t\tfree(key);\n+\t\t}\n+\n \t\tfinalize_commit_buffer(new_data, NULL, NULL, msg);\n \t} else {\n \t\tstrbuf_swap(new_data, &tmp_buf);\n@@ -2927,6 +2952,7 @@ static void parse_new_commit(const char *arg)\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n \t\tcase SIGN_STRIP_IF_INVALID:\n+\t\tcase SIGN_RESIGN_IF_INVALID:\n \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n@@ -3011,9 +3037,11 @@ static void parse_new_commit(const char *arg)\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n \n-\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n+\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n+\t     signed_commit_mode == SIGN_RESIGN_IF_INVALID) &&\n \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n-\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n+\t\thandle_invalid_signature(&new_data, &sig_sha1, &sig_sha256, &msg,\n+\t\t\t\t\t signed_commit_mode);\n \telse\n \t\tfinalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);\n \n@@ -3060,6 +3088,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\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_RESIGN_IF_INVALID:\n+\t\tdie(_(\"'re-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);\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 87fb6605fb..e7eb42d9d6 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1156,6 +1156,8 @@ int parse_sign_mode(const char *arg, enum sign_mode *mode)\n \t\t*mode = SIGN_STRIP;\n \telse if (!strcmp(arg, \"strip-if-invalid\"))\n \t\t*mode = SIGN_STRIP_IF_INVALID;\n+\telse if (!strcmp(arg, \"re-sign-if-invalid\"))\n+\t\t*mode = SIGN_RESIGN_IF_INVALID;\n \telse\n \t\treturn -1;\n \treturn 0;\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 789d1ffac4..2ab2a21e1a 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -112,6 +112,7 @@ enum sign_mode {\n \tSIGN_WARN_STRIP,\n \tSIGN_STRIP,\n \tSIGN_STRIP_IF_INVALID,\n+\tSIGN_RESIGN_IF_INVALID,\n };\n \n /*\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nindex 022dae02e4..b52fb75976 100755\n--- a/t/t9305-fast-import-signatures.sh\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -103,71 +103,81 @@ test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war\n \ttest_line_count = 2 out\n '\n \n-test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '\n-\tgit fast-export main >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n-\n-\t# Change the commit message, which invalidates the signature.\n-\t# The commit message length should not change though, otherwise the\n-\t# corresponding `data <length>` command would have to be changed too.\n-\tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n-\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n-\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING != $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep ! -E \"^gpgsig\" actual &&\n-\ttest_grep \"stripping invalid signature\" log\n-'\n-\n-test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n-\ttest $X509_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n-\n-\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n-\ttest $SSH_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n+for mode in strip-if-invalid re-sign-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+\t\tgit -C new fast-import --quiet --signed-commits=$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-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\t\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"strip signature invalidated by message change with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\n+\t\t# Change the commit message, which invalidates the signature.\n+\t\t# The commit message length should not change though, otherwise the\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\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+\t\ttest $OPENPGP_SIGNING != $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep \"stripping invalid signature\" log &&\n+\n+\t\tif test \"$mode\" = strip-if-invalid\n+\t\tthen\n+\t\t\ttest_grep ! -E \"^gpgsig\" actual\n+\t\telse\n+\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\t\tgit -C new verify-commit \"$IMPORTED\"\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success GPGSM \"keep valid X.509 signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\t\ttest $X509_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSSH \"keep valid SSH signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\t\ttest $SSH_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+done\n \n test_done\n-- \n2.53.0\n\n"},{"id":"536954","messageId":"aZ1wblYGQssyNYsk@pks.im","threadId":"65062","inReplyTo":"20260223194146.3476768-3-jltobler@gmail.com","subject":"Re: [PATCH 2/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-24T09:33:34Z","receivedAt":"2026-02-24T09:33:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Feb 23, 2026 at 01:41:46PM -0600, Justin Tobler wrote:\n> With git-fast-import(1), handling of signed commits is controlled via\n> the `--signed-commits=<mode>` option. When an invalid signature is\n> encountered, a user may want the option to re-sign the commit as opposed\n> to just stripping the signature. To faciliate this, introduce a\n> \"re-sign-if-invalid\" mode for the `--signed-commits` option.\n> \n> Note that commits are re-signed using only the repository object format\n> hash algorithm. If a commit has an additional signature due to the\n> `compatObjectFormat` repository extension being set, the other signature\n> is stripped.\n\nThis part here might use some explanation why this part is not done so\nthat a future reader that ends up here doesn't have to wonder whether\nthis is done with intent, or whether this was done because it was hard\nto do.\n\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 479c4081da..b902a6e2b0 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -86,6 +86,9 @@ already trusted to run their own code.\n>  * `strip-if-invalid` will check signatures and, if they are invalid,\n>    will strip them and display a warning. The validation is performed\n>    in the same way as linkgit:git-verify-commit[1] does it.\n> +* `re-sign-if-invalid` is the same as `strip-if-invalid`, but additionally the\n> +  commits with invalid signatures are signed again, so that old invalid\n> +  signatures are replaced with new valid ones.\n\nOkay. It's a bit curious to say it's the \"same as `strip-if-invalid`\",\nbut I get what you mean by this, and I think a user would, too.\n\n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index b8a7757cfd..e34a373d2f 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -2836,10 +2836,11 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n>  \tstrbuf_addbuf(new_data, msg);\n>  }\n>  \n> -static void handle_strip_if_invalid(struct strbuf *new_data,\n> -\t\t\t\t    struct signature_data *sig_sha1,\n> -\t\t\t\t    struct signature_data *sig_sha256,\n> -\t\t\t\t    struct strbuf *msg)\n> +static void handle_invalid_signature(struct strbuf *new_data,\n> +\t\t\t\t     struct signature_data *sig_sha1,\n> +\t\t\t\t     struct signature_data *sig_sha256,\n> +\t\t\t\t     struct strbuf *msg,\n> +\t\t\t\t     enum sign_mode mode)\n>  {\n>  \tstruct strbuf tmp_buf = STRBUF_INIT;\n>  \tstruct signature_check signature_check = { 0 };\n\nShould we maybe call this `handle_signature_if_invalid()`? Otherwise it\nsounds as if we already know the signature was invalid.\n\n> @@ -2866,6 +2867,30 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n>  \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n>  \t\t\t\t  \"  allegedly by %s\"), signer);\n\nI wonder: does it still make sense to warn about those stripped\nsignatures in case we re-sign anyway?\n\n> +\t\tif (mode == SIGN_RESIGN_IF_INVALID) {\n> +\t\t\tstruct strbuf signature = STRBUF_INIT;\n> +\t\t\tstruct strbuf payload = STRBUF_INIT;\n> +\t\t\tchar *key = get_signing_key();\n> +\n> +\t\t\t/*\n> +\t\t\t * Commits are resigned using the repository object\n\nPoor commits. Maybe s/resigned/re-signed/?\n\n> +\t\t\t * format hash algorithm only. Consequently if\n> +\t\t\t * extensions.compatObjectFormat is set, the\n> +\t\t\t * compatability hash is not currently used to\n> +\t\t\t * additionally sign the commit. If the commit payload\n> +\t\t\t * were reconstructed in the compatability format, it\n> +\t\t\t * would be possible to generate the other signature\n> +\t\t\t * accordingly though.\n> +\t\t\t */\n\nSame as in the commit message, we should document whether this is done\nintentionally, or whether it may require more work going forward. If the\nlatter, it might make sense to add a NEEDSWORK comment.\n\nI think meanwhile though it's okay that we don't handle compatibility\nhashes yet.\n\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 87fb6605fb..e7eb42d9d6 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -1156,6 +1156,8 @@ int parse_sign_mode(const char *arg, enum sign_mode *mode)\n>  \t\t*mode = SIGN_STRIP;\n>  \telse if (!strcmp(arg, \"strip-if-invalid\"))\n>  \t\t*mode = SIGN_STRIP_IF_INVALID;\n> +\telse if (!strcmp(arg, \"re-sign-if-invalid\"))\n> +\t\t*mode = SIGN_RESIGN_IF_INVALID;\n>  \telse\n>  \t\treturn -1;\n>  \treturn 0;\n\nOne thing I wonder here is which signing key is actually in use, and how\nthe user would specify it. In git-commit(1) you can for example pass\n\"--gpg-sign=<key-id>\" to specify the key. Do we want to allow the same\nhere, where you can pass \"--signed-commits=re-sign-if-invalid[=<gpg-key>]\"?\n\nThanks!\n\nPatrick\n"},{"id":"536956","messageId":"aZ1w1cOehTZ11hUI@pks.im","threadId":"65062","inReplyTo":"20260223194146.3476768-2-jltobler@gmail.com","subject":"Re: [PATCH 1/2] commit: remove unused forward declaration","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-24T09:35:17Z","receivedAt":"2026-02-24T09:35:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Feb 23, 2026 at 01:41:45PM -0600, Justin Tobler wrote:\n> In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n> `sign_with_header()` was removed, but its forward declaration in\n> \"commit.h\" was left. Remove the unused declaration.\n\nIndeed, the definition of that function doesn't exist anymore.\n\nPatrick\n"},{"id":"536972","messageId":"CAP8UFD0OP3BP2RxiL2ip8WEC8SqT5LGH5dSco-2Jwzsd_4=60g@mail.gmail.com","threadId":"65062","inReplyTo":"20260223194146.3476768-1-jltobler@gmail.com","subject":"Re: [PATCH 0/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-02-24T13:40:44Z","receivedAt":"2026-02-24T13:40:56Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Hi,\n\nOn Mon, Feb 23, 2026 at 8:42 PM Justin Tobler <jltobler@gmail.com> wrote:\n>\n> Greetings,\n>\n> With c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n> --signed-commits=<mode>, 2025-11-17), it became possible to remove\n> invalid signatures from commits via git-fast-import(1) while maintaining\n> valid commits. Building upon this functionality, a user may want to\n\ns/valid commits/valid commit signatures/\n\n> re-sign these invalid commit signatures. This series introduces the\n> `re-sign-if-invalid` mode to do so accordingly.\n>\n> The newly added mode in this series currently ignores\n> `extensions.compatObjectFormat` when generating the new signatures. From\n> my understanding, to generate the compatability structure would also\n\nHere and below: s/compatability/compatibility/\n\n> require us to reconstruct the compatability object for the object being\n> signed. I think this would be possible to do, but would require getting\n> the mapped OIDs for the commit parents and tree. I'm not competely sure\n\ns/competely/completely/\n\n> of a good way to go about this yet though. I'm also not completely\n> certain if this is something that should be adressed as part of this\n\ns/adressed/addressed/\n\n> series, or could be done later down the road. So for now I've opted to\n> delay its implementation. I'm open going down the other route if that is\n> preferred though.\n\nThat's a reasonable approach to me.\n\nThanks for taking over this.\n"},{"id":"536987","messageId":"aZ3sI5LAj-bSt0Oy@denethor","threadId":"65062","inReplyTo":"aZ1wblYGQssyNYsk@pks.im","subject":"Re: [PATCH 2/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-02-24T18:33:52Z","receivedAt":"2026-02-24T18:33:58Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/02/24 10:33AM, Patrick Steinhardt wrote:\n> On Mon, Feb 23, 2026 at 01:41:46PM -0600, Justin Tobler wrote:\n> > With git-fast-import(1), handling of signed commits is controlled via\n> > the `--signed-commits=<mode>` option. When an invalid signature is\n> > encountered, a user may want the option to re-sign the commit as opposed\n> > to just stripping the signature. To faciliate this, introduce a\n> > \"re-sign-if-invalid\" mode for the `--signed-commits` option.\n> > \n> > Note that commits are re-signed using only the repository object format\n> > hash algorithm. If a commit has an additional signature due to the\n> > `compatObjectFormat` repository extension being set, the other signature\n> > is stripped.\n> \n> This part here might use some explanation why this part is not done so\n> that a future reader that ends up here doesn't have to wonder whether\n> this is done with intent, or whether this was done because it was hard\n> to do.\n\nGood point. I'll expand the explaination here in the next version.\n\n> > diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> > index 479c4081da..b902a6e2b0 100644\n> > --- a/Documentation/git-fast-import.adoc\n> > +++ b/Documentation/git-fast-import.adoc\n> > @@ -86,6 +86,9 @@ already trusted to run their own code.\n> >  * `strip-if-invalid` will check signatures and, if they are invalid,\n> >    will strip them and display a warning. The validation is performed\n> >    in the same way as linkgit:git-verify-commit[1] does it.\n> > +* `re-sign-if-invalid` is the same as `strip-if-invalid`, but additionally the\n> > +  commits with invalid signatures are signed again, so that old invalid\n> > +  signatures are replaced with new valid ones.\n> \n> Okay. It's a bit curious to say it's the \"same as `strip-if-invalid`\",\n> but I get what you mean by this, and I think a user would, too.\n\nYa, maybe it would be better to say that it is \"similar to\n`strip-if-invalid`\". I'll try to rework the documentation here a little\nbit in the next version.\n\n> > diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> > index b8a7757cfd..e34a373d2f 100644\n> > --- a/builtin/fast-import.c\n> > +++ b/builtin/fast-import.c\n> > @@ -2836,10 +2836,11 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n> >  \tstrbuf_addbuf(new_data, msg);\n> >  }\n> >  \n> > -static void handle_strip_if_invalid(struct strbuf *new_data,\n> > -\t\t\t\t    struct signature_data *sig_sha1,\n> > -\t\t\t\t    struct signature_data *sig_sha256,\n> > -\t\t\t\t    struct strbuf *msg)\n> > +static void handle_invalid_signature(struct strbuf *new_data,\n> > +\t\t\t\t     struct signature_data *sig_sha1,\n> > +\t\t\t\t     struct signature_data *sig_sha256,\n> > +\t\t\t\t     struct strbuf *msg,\n> > +\t\t\t\t     enum sign_mode mode)\n> >  {\n> >  \tstruct strbuf tmp_buf = STRBUF_INIT;\n> >  \tstruct signature_check signature_check = { 0 };\n> \n> Should we maybe call this `handle_signature_if_invalid()`? Otherwise it\n> sounds as if we already know the signature was invalid.\n\nThat sounds better. Will adapt.\n\n> \n> > @@ -2866,6 +2867,30 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n> >  \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n> >  \t\t\t\t  \"  allegedly by %s\"), signer);\n> \n> I wonder: does it still make sense to warn about those stripped\n> signatures in case we re-sign anyway?\n\nYa, good point. I was originally thinking it would still make sense to\nkeep these messages since we are still stripping the signatures, but it\nmight be misleading if are also re-signing them. We could keep these,\nbut add an additional message if re-signing. That might be a little\nnoisy though. Maybe we just adapt the warning message when re-signing.\n\n> > +\t\tif (mode == SIGN_RESIGN_IF_INVALID) {\n> > +\t\t\tstruct strbuf signature = STRBUF_INIT;\n> > +\t\t\tstruct strbuf payload = STRBUF_INIT;\n> > +\t\t\tchar *key = get_signing_key();\n> > +\n> > +\t\t\t/*\n> > +\t\t\t * Commits are resigned using the repository object\n> \n> Poor commits. Maybe s/resigned/re-signed/?\n\nPoor commits indeed, will change. XD\n\n> > +\t\t\t * format hash algorithm only. Consequently if\n> > +\t\t\t * extensions.compatObjectFormat is set, the\n> > +\t\t\t * compatability hash is not currently used to\n> > +\t\t\t * additionally sign the commit. If the commit payload\n> > +\t\t\t * were reconstructed in the compatability format, it\n> > +\t\t\t * would be possible to generate the other signature\n> > +\t\t\t * accordingly though.\n> > +\t\t\t */\n> \n> Same as in the commit message, we should document whether this is done\n> intentionally, or whether it may require more work going forward. If the\n> latter, it might make sense to add a NEEDSWORK comment.\n\nYa, I think it should be possible to support compatibility hashes in the\nfuture. I'll explain this better in a NEEDSWORK comment.\n\n> I think meanwhile though it's okay that we don't handle compatibility\n> hashes yet.\n> \n> > diff --git a/gpg-interface.c b/gpg-interface.c\n> > index 87fb6605fb..e7eb42d9d6 100644\n> > --- a/gpg-interface.c\n> > +++ b/gpg-interface.c\n> > @@ -1156,6 +1156,8 @@ int parse_sign_mode(const char *arg, enum sign_mode *mode)\n> >  \t\t*mode = SIGN_STRIP;\n> >  \telse if (!strcmp(arg, \"strip-if-invalid\"))\n> >  \t\t*mode = SIGN_STRIP_IF_INVALID;\n> > +\telse if (!strcmp(arg, \"re-sign-if-invalid\"))\n> > +\t\t*mode = SIGN_RESIGN_IF_INVALID;\n> >  \telse\n> >  \t\treturn -1;\n> >  \treturn 0;\n> \n> One thing I wonder here is which signing key is actually in use, and how\n> the user would specify it. In git-commit(1) you can for example pass\n> \"--gpg-sign=<key-id>\" to specify the key. Do we want to allow the same\n> here, where you can pass \"--signed-commits=re-sign-if-invalid[=<gpg-key>]\"?\n\nThis seems sensible. I'll explore this in the next version.\n\nThanks for the review. :)\n\n-Justin\n"},{"id":"537035","messageId":"aZ4pFUJApZosh9Gc@fruit.crustytoothpaste.net","threadId":"65062","inReplyTo":"20260223194146.3476768-1-jltobler@gmail.com","subject":"Re: [PATCH 0/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-02-24T22:41:25Z","receivedAt":"2026-02-24T22:41:26Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2026-02-23 at 19:41:44, Justin Tobler wrote:\n> Greetings,\n> \n> With c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n> --signed-commits=<mode>, 2025-11-17), it became possible to remove\n> invalid signatures from commits via git-fast-import(1) while maintaining\n> valid commits. Building upon this functionality, a user may want to\n> re-sign these invalid commit signatures. This series introduces the\n> `re-sign-if-invalid` mode to do so accordingly.\n> \n> The newly added mode in this series currently ignores\n> `extensions.compatObjectFormat` when generating the new signatures. From\n> my understanding, to generate the compatability structure would also\n> require us to reconstruct the compatability object for the object being\n> signed. I think this would be possible to do, but would require getting\n> the mapped OIDs for the commit parents and tree. I'm not competely sure\n> of a good way to go about this yet though. I'm also not completely\n> certain if this is something that should be adressed as part of this\n> series, or could be done later down the road. So for now I've opted to\n> delay its implementation. I'm open going down the other route if that is\n> preferred though.\n\nThere's an API for converting object IDs to another algorithm:\n`repo_oid_to_algop`.  If you want to convert a non-blob object, there's\n`convert_object_file`, which will serialize the object in the other\nformat (blobs are invariant in the hash algorithm transformation, so\nconverting them is not necessary).  Those are present right now in the\ncodebase and using them would be a good idea.\n\nIf you want to test your code in interoperability mode, you can rebase\nonto the `sha256-interop` branch of https://github.com/bk2204/git.git\nand run with `GIT_TEST_DEFAULT_HASH=sha256:sha1`.\n\nIf you're _not_ going to implement that in interoperability mode, then\nI'd rather you just die in that case so that the test fails and then I\nor someone else will fix it.  `extensions.compatObjectFormat` is\npresently experimental and the data formats will change, so nobody\nshould be relying on it working as it stands right now.  There _will_ be\nmore compatibility breakage coming in future series, for instance.\n\nI _would_ recommend regardless that you add a test like in t7004's\n\"signed tag with embedded PGP message\" if you apply this to tags as well\nas commits.  That requires a special case in our interoperability code\n(since it normally converts things that look like signatures, but when\nwe're _generating_ a tag, we don't want to do that since there are no\nsignatures yet) and making sure we do the same thing in fast-import will\navoid corruption in our conversions.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"537038","messageId":"xmqqwm01dam3.fsf@gitster.g","threadId":"65062","inReplyTo":"aZ4pFUJApZosh9Gc@fruit.crustytoothpaste.net","subject":"Re: [PATCH 0/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-24T22:45:24Z","receivedAt":"2026-02-24T22:45:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> If you're _not_ going to implement that in interoperability mode, then\n> I'd rather you just die in that case so that the test fails and then I\n> or someone else will fix it.  `extensions.compatObjectFormat` is\n> presently experimental and the data formats will change, so nobody\n> should be relying on it working as it stands right now.  There _will_ be\n> more compatibility breakage coming in future series, for instance.\n\nIt sounds like a very prudent thing to do to die as unsupported.\nThanks!\n\n> I _would_ recommend regardless that you add a test like in t7004's\n> \"signed tag with embedded PGP message\" if you apply this to tags as well\n> as commits.  That requires a special case in our interoperability code\n> (since it normally converts things that look like signatures, but when\n> we're _generating_ a tag, we don't want to do that since there are no\n> signatures yet) and making sure we do the same thing in fast-import will\n> avoid corruption in our conversions.\n"},{"id":"537626","messageId":"aaYStamdm-LCiaP-@denethor","threadId":"65062","inReplyTo":"aZ4pFUJApZosh9Gc@fruit.crustytoothpaste.net","subject":"Re: [PATCH 0/2] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-02T22:49:33Z","receivedAt":"2026-03-02T22:49:34Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/02/24 10:41PM, brian m. carlson wrote:\n> If you're _not_ going to implement that in interoperability mode, then\n> I'd rather you just die in that case so that the test fails and then I\n> or someone else will fix it.  `extensions.compatObjectFormat` is\n> presently experimental and the data formats will change, so nobody\n> should be relying on it working as it stands right now.  There _will_ be\n> more compatibility breakage coming in future series, for instance.\n\nThat sounds very sensible. In the next version I'll update to instead\ndie() as unsupported if we attempt to re-sign commit signatures in\ninteroperability mode.\n\n> I _would_ recommend regardless that you add a test like in t7004's\n> \"signed tag with embedded PGP message\" if you apply this to tags as well\n> as commits.  That requires a special case in our interoperability code\n> (since it normally converts things that look like signatures, but when\n> we're _generating_ a tag, we don't want to do that since there are no\n> signatures yet) and making sure we do the same thing in fast-import will\n> avoid corruption in our conversions.\n\nThanks, I'll look into this. This patch series currently only applies\nthis new mode to commits, but I plan to tackle tag signatures in a\nseparate followup series.\n\nThanks,\n-Justin\n"},{"id":"538106","messageId":"20260306205359.1723254-2-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260306205359.1723254-1-jltobler@gmail.com","subject":"[PATCH v2 1/3] commit: remove unused forward declaration","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-06T20:53:57Z","receivedAt":"2026-03-06T20:54:06Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n`sign_with_header()` was removed, but its forward declaration in\n\"commit.h\" was left. Remove the unused declaration.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.h | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/commit.h b/commit.h\nindex 1635de418b..f0c38cb444 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -390,8 +390,6 @@ LAST_ARG_MUST_BE_NULL\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook, const char *name, ...);\n \n-/* Sign a commit or tag buffer, storing the result in a header. */\n-int sign_with_header(struct strbuf *buf, const char *keyid);\n /* Parse the signature out of a header. */\n int parse_buffer_signed_by_header(const char *buffer,\n \t\t\t\t  unsigned long size,\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538107","messageId":"20260306205359.1723254-1-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260223194146.3476768-1-jltobler@gmail.com","subject":"[PATCH v2 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-06T20:53:56Z","receivedAt":"2026-03-06T20:54:06Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nWith c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17), it became possible to remove\ninvalid signatures from commits via git-fast-import(1) while maintaining\nvalid commit signatures. Building upon this functionality, a user may\nwant to re-sign these invalid commit signatures. This series introduces\nthe `re-sign-if-invalid` mode to do so accordingly.\n\nThe newly added mode in this series currently ignores\n`extensions.compatObjectFormat` when generating the new signatures. From\nmy understanding, to generate the compatibility structure would also\nrequire us to reconstruct the compatibility object for the object being\nsigned. I think this would be possible to do, but would require getting\nthe mapped OIDs for the commit parents and tree. I'm not completely sure\nof a good way to go about this yet though. I'm also not completely\ncertain if this is something that should be addressed as part of this\nseries, or could be done later down the road. So for now I've opted to\ndelay its implementation. I'm open going down the other route if that is\npreferred though.\n\nThe first commit is a simple cleanup for something I noticed while\nreading though commit signing code. The second commit actually\nintroduces the new `--signed-commits` mode.\n\nChanges since V1:\n- Improved commit messages and comments to better explain why\n  interoperability mode is not currently supported.\n- Clarified documentation for re-sign-if-invalid mode.\n- Renamed `handle_invalid_signature()` to `handle_signature_if_invalid()`.\n- Added warning messages specific to commit resigning.\n- Fixed some small typos.\n- Added support for explicitly specifying the signing key ID via\n  `--signed-commits=re-sign-if-invalid[=<keyid>]` similar to how it can\n  specified in git-commit(1).\n- We now die() as unsupported when attempting to re-sign an invalid\n  commit signature in interoperability mode.\n- We now die() when failing to re-sign a commit.\n\nThanks,\n-Justin\n\nJustin Tobler (3):\n  commit: remove unused forward declaration\n  gpg-interface: introduce sign_buffer_with_key()\n  fast-import: add mode to re-sign invalid commit signatures\n\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              |  79 ++++++++++++----\n commit.c                           |  16 +---\n commit.h                           |   2 -\n gpg-interface.c                    |  36 ++++++--\n gpg-interface.h                    |  14 ++-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 8 files changed, 205 insertions(+), 94 deletions(-)\n\nRange-diff against v1:\n1:  0d00b72ee0 = 1:  0d00b72ee0 commit: remove unused forward declaration\n-:  ---------- > 2:  499025532c gpg-interface: introduce sign_buffer_with_key()\n2:  16e4022616 ! 3:  bea1a42eb9 fast-import: add mode to re-sign invalid commit signatures\n    @@ Commit message\n         With git-fast-import(1), handling of signed commits is controlled via\n         the `--signed-commits=<mode>` option. When an invalid signature is\n         encountered, a user may want the option to re-sign the commit as opposed\n    -    to just stripping the signature. To faciliate this, introduce a\n    -    \"re-sign-if-invalid\" mode for the `--signed-commits` option.\n    +    to just stripping the signature. To facilitate this, introduce a\n    +    \"re-sign-if-invalid\" mode for the `--signed-commits` option. Optionally,\n    +    a key ID may be explicitly provided in the form\n    +    `re-sign-if-invalid[=<keyid>]` to specify which signing key should be\n    +    used when re-signing invalid commit signatures.\n     \n    -    Note that commits are re-signed using only the repository object format\n    -    hash algorithm. If a commit has an additional signature due to the\n    -    `compatObjectFormat` repository extension being set, the other signature\n    -    is stripped.\n    +    Note that to properly support interoperability mode when re-signing\n    +    commit signatures, the commit buffer must be created in both the\n    +    repository and compatability object formats to generate the appropriate\n    +    signatures accordingly. As currently implemented, the commit buffer for\n    +    the compatability object format is not reconstructed and thus re-signing\n    +    commits in interoperability mode is not yet supported. Support may be\n    +    added in the future.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n    @@ Documentation/git-fast-import.adoc: already trusted to run their own code.\n      * `strip-if-invalid` will check signatures and, if they are invalid,\n        will strip them and display a warning. The validation is performed\n        in the same way as linkgit:git-verify-commit[1] does it.\n    -+* `re-sign-if-invalid` is the same as `strip-if-invalid`, but additionally the\n    -+  commits with invalid signatures are signed again, so that old invalid\n    -+  signatures are replaced with new valid ones.\n    ++* `re-sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\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 re-signing; otherwise the configured default signing key is used.\n      \n      Options for Frontends\n      ~~~~~~~~~~~~~~~~~~~~~\n     \n      ## builtin/fast-export.c ##\n    +@@ builtin/fast-export.c: 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))\n    ++\tif (parse_sign_mode(arg, val, NULL))\n    + \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n    + \n    + \treturn 0;\n     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct rev_info *rev,\n      \t\tcase SIGN_STRIP_IF_INVALID:\n      \t\t\tdie(_(\"'strip-if-invalid' is not a valid mode for \"\n    @@ builtin/fast-export.c: static void handle_tag(const char *name, struct tag *tag)\n      \t\t\t}\n     \n      ## builtin/fast-import.c ##\n    +@@ builtin/fast-import.c: static const char *global_prefix;\n    + \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    + \n    + /* Memory pools */\n    + static struct mem_pool fi_mem_pool = {\n     @@ builtin/fast-import.c: static void finalize_commit_buffer(struct strbuf *new_data,\n      \tstrbuf_addbuf(new_data, msg);\n      }\n    @@ builtin/fast-import.c: static void finalize_commit_buffer(struct strbuf *new_dat\n     -\t\t\t\t    struct signature_data *sig_sha1,\n     -\t\t\t\t    struct signature_data *sig_sha256,\n     -\t\t\t\t    struct strbuf *msg)\n    -+static void handle_invalid_signature(struct strbuf *new_data,\n    -+\t\t\t\t     struct signature_data *sig_sha1,\n    -+\t\t\t\t     struct signature_data *sig_sha256,\n    -+\t\t\t\t     struct strbuf *msg,\n    -+\t\t\t\t     enum sign_mode mode)\n    ++static void handle_signature_if_invalid(struct strbuf *new_data,\n    ++\t\t\t\t\tstruct signature_data *sig_sha1,\n    ++\t\t\t\t\tstruct signature_data *sig_sha256,\n    ++\t\t\t\t\tstruct strbuf *msg,\n    ++\t\t\t\t\tenum sign_mode mode)\n      {\n      \tstruct strbuf tmp_buf = STRBUF_INIT;\n      \tstruct signature_check signature_check = { 0 };\n     @@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_data,\n    - \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n    - \t\t\t\t  \"  allegedly by %s\"), signer);\n    + \t\tconst char *subject;\n    + \t\tint subject_len = find_commit_subject(msg->buf, &subject);\n      \n    -+\t\tif (mode == SIGN_RESIGN_IF_INVALID) {\n    +-\t\tif (subject_len > 100)\n    +-\t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n    +-\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    +-\t\telse if (subject_len > 0)\n    +-\t\t\twarning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n    +-\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n    +-\t\telse\n    +-\t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n    +-\t\t\t\t  \"  allegedly by %s\"), signer);\n    ++\t\tif (mode == SIGN_STRIP_IF_INVALID) {\n    ++\t\t\tif (subject_len > 100)\n    ++\t\t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n    ++\t\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    ++\t\t\telse if (subject_len > 0)\n    ++\t\t\t\twarning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n    ++\t\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n    ++\t\t\telse\n    ++\t\t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n    ++\t\t\t\t\t  \"  allegedly by %s\"), signer);\n    ++\t\t} else if (mode == SIGN_RESIGN_IF_INVALID) {\n     +\t\t\tstruct strbuf signature = STRBUF_INIT;\n     +\t\t\tstruct strbuf payload = STRBUF_INIT;\n    -+\t\t\tchar *key = get_signing_key();\n    ++\n    ++\t\t\tif (subject_len > 100)\n    ++\t\t\t\twarning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n    ++\t\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    ++\t\t\telse if (subject_len > 0)\n    ++\t\t\t\twarning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n    ++\t\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n    ++\t\t\telse\n    ++\t\t\t\twarning(_(\"re-signing invalid signature for commit\\n\"\n    ++\t\t\t\t\t  \"  allegedly by %s\"), signer);\n     +\n     +\t\t\t/*\n    -+\t\t\t * Commits are resigned using the repository object\n    -+\t\t\t * format hash algorithm only. Consequently if\n    -+\t\t\t * extensions.compatObjectFormat is set, the\n    -+\t\t\t * compatability hash is not currently used to\n    -+\t\t\t * additionally sign the commit. If the commit payload\n    -+\t\t\t * were reconstructed in the compatability format, it\n    -+\t\t\t * would be possible to generate the other signature\n    -+\t\t\t * accordingly though.\n    ++\t\t\t * NEEDSWORK: To properly support interoperability mode\n    ++\t\t\t * when re-signing commit signatures, the commit buffer\n    ++\t\t\t * must be provided in both the repository and\n    ++\t\t\t * compatability object formats. As currently\n    ++\t\t\t * implemented, only the repository object format is\n    ++\t\t\t * considered meaning compatability signatures cannot be\n    ++\t\t\t * generated. Thus, attempting to re-sign commit\n    ++\t\t\t * signatures in interoperability mode is currently\n    ++\t\t\t * unsupported.\n     +\t\t\t */\n    ++\t\t\tif (the_repository->compat_hash_algo)\n    ++\t\t\t\tdie(_(\"re-signing signatures in interoperability mode is unsupported\"));\n    ++\n     +\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n    -+\t\t\tsign_buffer(&payload, &signature, key);\n    ++\t\t\tif (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))\n    ++\t\t\t\tdie(_(\"failed to sign commit object\"));\n     +\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n     +\n     +\t\t\tstrbuf_release(&signature);\n     +\t\t\tstrbuf_release(&payload);\n    -+\t\t\tfree(key);\n     +\t\t}\n    -+\n    + \n      \t\tfinalize_commit_buffer(new_data, NULL, NULL, msg);\n      \t} else {\n    - \t\tstrbuf_swap(new_data, &tmp_buf);\n     @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n      \t\t\t/* fallthru */\n      \t\tcase SIGN_VERBATIM:\n    @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n     +\t     signed_commit_mode == SIGN_RESIGN_IF_INVALID) &&\n      \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n     -\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n    -+\t\thandle_invalid_signature(&new_data, &sig_sha1, &sig_sha256, &msg,\n    -+\t\t\t\t\t signed_commit_mode);\n    ++\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n    ++\t\t\t\t\t    &msg, signed_commit_mode);\n      \telse\n      \t\tfinalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);\n      \n    @@ builtin/fast-import.c: static void handle_tag_signature(struct strbuf *msg, cons\n      \tdefault:\n      \t\tBUG(\"invalid signed_tag_mode value %d from tag '%s'\",\n      \t\t    signed_tag_mode, name);\n    +@@ builtin/fast-import.c: static int parse_one_option(const char *option)\n    + \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n    + \t\toption_export_pack_edges(option);\n    + \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n    +-\t\tif (parse_sign_mode(option, &signed_commit_mode))\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))\n    ++\t\tif (parse_sign_mode(option, &signed_tag_mode, NULL))\n    + \t\t\tusagef(_(\"unknown --signed-tags mode '%s'\"), option);\n    + \t} else if (!strcmp(option, \"quiet\")) {\n    + \t\tshow_stats = 0;\n     \n      ## gpg-interface.c ##\n    -@@ gpg-interface.c: int parse_sign_mode(const char *arg, enum sign_mode *mode)\n    +@@ gpg-interface.c: static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n    + \treturn ret;\n    + }\n    + \n    +-int parse_sign_mode(const char *arg, enum sign_mode *mode)\n    ++int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)\n    + {\n    +-\tif (!strcmp(arg, \"abort\"))\n    ++\tif (!strcmp(arg, \"abort\")) {\n    + \t\t*mode = SIGN_ABORT;\n    +-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n    ++\t} else if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\")) {\n    + \t\t*mode = SIGN_VERBATIM;\n    +-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n    ++\t} else if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\")) {\n    + \t\t*mode = SIGN_WARN_VERBATIM;\n    +-\telse if (!strcmp(arg, \"warn-strip\"))\n    ++\t} else if (!strcmp(arg, \"warn-strip\")) {\n    + \t\t*mode = SIGN_WARN_STRIP;\n    +-\telse if (!strcmp(arg, \"strip\"))\n    ++\t} else if (!strcmp(arg, \"strip\")) {\n      \t\t*mode = SIGN_STRIP;\n    - \telse if (!strcmp(arg, \"strip-if-invalid\"))\n    +-\telse if (!strcmp(arg, \"strip-if-invalid\"))\n    ++\t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n      \t\t*mode = SIGN_STRIP_IF_INVALID;\n    -+\telse if (!strcmp(arg, \"re-sign-if-invalid\"))\n    +-\telse\n    ++\t} else if (!strcmp(arg, \"re-sign-if-invalid\")) {\n     +\t\t*mode = SIGN_RESIGN_IF_INVALID;\n    - \telse\n    ++\t} else if (skip_prefix(arg, \"re-sign-if-invalid=\", &arg)) {\n    ++\t\t*mode = SIGN_RESIGN_IF_INVALID;\n    ++\t\tif (keyid)\n    ++\t\t\t*keyid = arg;\n    ++\t} else {\n      \t\treturn -1;\n    ++\t}\n      \treturn 0;\n    + }\n     \n      ## gpg-interface.h ##\n     @@ gpg-interface.h: enum sign_mode {\n    @@ gpg-interface.h: enum sign_mode {\n      };\n      \n      /*\n    +  * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n    +- * otherwise.\n    ++ * otherwise. If the parsed mode is SIGN_RESIGN_IF_INVALID and GPG key provided\n    ++ * in the arguments in the form `re-sign-if-invalid=<keyid>`, the key-ID is\n    ++ * parsed into `char **keyid`.\n    +  */\n    +-int parse_sign_mode(const char *arg, enum sign_mode *mode);\n    ++int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);\n    + \n    + #endif\n     \n      ## t/t9305-fast-import-signatures.sh ##\n     @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     -'\n     -\n     -test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n    --\trm -rf new &&\n    --\tgit init new &&\n    --\n    --\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n    --\n    --\t# Change the commit message, which invalidates the signature.\n    --\t# The commit message length should not change though, otherwise the\n    --\t# corresponding `data <length>` command would have to be changed too.\n    --\tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n    --\n    --\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n    --\n    --\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n    --\ttest $OPENPGP_SIGNING != $IMPORTED &&\n    --\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    --\ttest_grep ! -E \"^gpgsig\" actual &&\n    --\ttest_grep \"stripping invalid signature\" log\n    --'\n    --\n    --test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n    --\trm -rf new &&\n    --\tgit init new &&\n    --\n    --\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n    --\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n    --\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n    --\ttest $X509_SIGNING = $IMPORTED &&\n    --\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    --\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n    --\ttest_must_be_empty log\n    --'\n    --\n    --test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n    --\trm -rf new &&\n    --\tgit init new &&\n    --\n    --\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n    --\n    --\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n    --\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n    --\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n    --\ttest $SSH_SIGNING = $IMPORTED &&\n    --\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    --\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n    --\ttest_must_be_empty log\n    --'\n     +for mode in strip-if-invalid re-sign-if-invalid\n     +do\n     +\ttest_expect_success GPG \"import commit with no signature with --signed-commits=$mode\" '\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     +\t\ttest_must_be_empty log\n     +\t'\n     +\n    -+\ttest_expect_success GPG \"strip signature invalidated by message change with --signed-commits=$mode\" '\n    ++\ttest_expect_success GPG \"handle signature invalidated by message change with --signed-commits=$mode\" '\n     +\t\trm -rf new &&\n     +\t\tgit init new &&\n     +\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     +\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n     +\t\ttest $OPENPGP_SIGNING != $IMPORTED &&\n     +\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    -+\t\ttest_grep \"stripping invalid signature\" log &&\n     +\n     +\t\tif test \"$mode\" = strip-if-invalid\n     +\t\tthen\n    ++\t\t\ttest_grep \"stripping invalid signature\" log &&\n     +\t\t\ttest_grep ! -E \"^gpgsig\" actual\n     +\t\telse\n    ++\t\t\ttest_grep \"re-signing invalid signature\" log &&\n     +\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n     +\t\t\tgit -C new verify-commit \"$IMPORTED\"\n     +\t\tfi\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     +\t\ttest_must_be_empty log\n     +\t'\n     +done\n    ++\n    ++test_expect_success GPGSSH \"re-sign invalid commit with explicit keyid\" '\n    + \trm -rf new &&\n    + \tgit init new &&\n    + \n    +@@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip signature invalidated by message change with --si\n    + \t# corresponding `data <length>` command would have to be changed too.\n    + \tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n    + \n    +-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n    ++\t# Configure the target repository with an invalid default signing key.\n    ++\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n    ++\ttest_config -C new gpg.format ssh &&\n    ++\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n    ++\ttest_must_fail git -C new fast-import --quiet \\\n    ++\t\t--signed-commits=re-sign-if-invalid <modified >/dev/null 2>&1 &&\n    ++\n    ++\t# Import using explicitly provided signing key.\n    ++\tgit -C new fast-import --quiet \\\n    ++\t\t--signed-commits=re-sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n    + \n    + \tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n    + \ttest $OPENPGP_SIGNING != $IMPORTED &&\n    + \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    +-\ttest_grep ! -E \"^gpgsig\" actual &&\n    +-\ttest_grep \"stripping invalid signature\" log\n    +-'\n    +-\n    +-test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n    +-\trm -rf new &&\n    +-\tgit init new &&\n    +-\n    +-\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n    +-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n    +-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n    +-\ttest $X509_SIGNING = $IMPORTED &&\n    +-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    + \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n    +-\ttest_must_be_empty log\n    +-'\n    +-\n    +-test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n    +-\trm -rf new &&\n    +-\tgit init new &&\n    +-\n    +-\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n    +-\n    +-\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n    +-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n    +-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n    +-\ttest $SSH_SIGNING = $IMPORTED &&\n    +-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    +-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n    +-\ttest_must_be_empty log\n    ++\tgit -C new verify-commit \"$IMPORTED\"\n    + '\n      \n      test_done\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538108","messageId":"20260306205359.1723254-3-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260306205359.1723254-1-jltobler@gmail.com","subject":"[PATCH v2 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-06T20:53:58Z","receivedAt":"2026-03-06T20:54:07Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\nlogic to get the default configured signing key when a key is not\nprovided and handles generating the commit signature accordingly. This\nsigning operation is not really specific to commits as any arbitrary\nbuffer can be signed. Also, in a subsequent commit, this same logic is\nreused by git-fast-import(1) when resigning invalid commit signatures.\nIntroduce `sign_buffer_with_key()` to centralize signing key resolution\nin gpg-interface to allow callers to reuse the same behavior without\nduplicating logic.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.c        | 16 ++--------------\n gpg-interface.c | 13 +++++++++++++\n gpg-interface.h |  7 +++++++\n 3 files changed, 22 insertions(+), 14 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex d16ae73345..1677b1ef25 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1148,18 +1148,6 @@ int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi\n \treturn 0;\n }\n \n-static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)\n-{\n-\tchar *keyid_to_free = NULL;\n-\tint ret = 0;\n-\tif (!keyid || !*keyid)\n-\t\tkeyid = keyid_to_free = get_signing_key();\n-\tif (sign_buffer(buf, sig, keyid))\n-\t\tret = -1;\n-\tfree(keyid_to_free);\n-\treturn ret;\n-}\n-\n int parse_signed_commit(const struct commit *commit,\n \t\t\tstruct strbuf *payload, struct strbuf *signature,\n \t\t\tconst struct git_hash_algo *algop)\n@@ -1737,7 +1725,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\toidcpy(&parent_buf[i++], &p->item->object.oid);\n \n \twrite_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);\n-\tif (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {\n+\tif (sign_commit && sign_buffer_with_key(&buffer, &sig, sign_commit)) {\n \t\tresult = -1;\n \t\tgoto out;\n \t}\n@@ -1769,7 +1757,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\tfree_commit_extra_headers(compat_extra);\n \t\tfree(mapped_parents);\n \n-\t\tif (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {\n+\t\tif (sign_commit && sign_buffer_with_key(&compat_buffer, &compat_sig, sign_commit)) {\n \t\t\tresult = -1;\n \t\t\tgoto out;\n \t\t}\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 87fb6605fb..a72fa35061 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -980,6 +980,19 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t const char *signing_key)\n+{\n+\tchar *keyid_to_free = NULL;\n+\tint ret = 0;\n+\tif (!signing_key || !*signing_key)\n+\t\tsigning_key = keyid_to_free = get_signing_key();\n+\tif (sign_buffer(buffer, signature, signing_key))\n+\t\tret = -1;\n+\tfree(keyid_to_free);\n+\treturn ret;\n+}\n+\n /*\n  * Strip CR from the line endings, in case we are on Windows.\n  * NEEDSWORK: make it trim only CRs before LFs and rename\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 789d1ffac4..a32741aeda 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -83,6 +83,13 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n \t\tconst char *signing_key);\n \n+/*\n+ * Similar to `sign_buffer()`, but uses the default configured signing key as\n+ * returned by `get_signing_key()` when the provided \"signing_key\" is NULL or\n+ * empty. Returns 0 on success, non-zero on failure.\n+ */\n+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t const char *signing_key);\n \n /*\n  * Returns corresponding string in lowercase for a given member of\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538109","messageId":"20260306205359.1723254-4-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260306205359.1723254-1-jltobler@gmail.com","subject":"[PATCH v2 3/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-06T20:53:59Z","receivedAt":"2026-03-06T20:54:08Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"With git-fast-import(1), handling of signed commits is controlled via\nthe `--signed-commits=<mode>` option. When an invalid signature is\nencountered, a user may want the option to re-sign the commit as opposed\nto just stripping the signature. To facilitate this, introduce a\n\"re-sign-if-invalid\" mode for the `--signed-commits` option. Optionally,\na key ID may be explicitly provided in the form\n`re-sign-if-invalid[=<keyid>]` to specify which signing key should be\nused when re-signing invalid commit signatures.\n\nNote that to properly support interoperability mode when re-signing\ncommit signatures, the commit buffer must be created in both the\nrepository and compatability object formats to generate the appropriate\nsignatures accordingly. As currently implemented, the commit buffer for\nthe compatability object format is not reconstructed and thus re-signing\ncommits in interoperability mode is not yet supported. Support may be\nadded in the future.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              |  79 ++++++++++++----\n gpg-interface.c                    |  23 +++--\n gpg-interface.h                    |   7 +-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 6 files changed, 183 insertions(+), 78 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 479c4081da..08f7d5d89a 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -86,6 +86,10 @@ already trusted to run their own code.\n * `strip-if-invalid` will check signatures and, if they are invalid,\n   will strip them and display a warning. The validation is performed\n   in the same way as linkgit:git-verify-commit[1] does it.\n+* `re-sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\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 re-signing; otherwise the configured default signing key is used.\n \n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 0c5d2386d8..0ab8465ae3 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -64,7 +64,7 @@ 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))\n+\tif (parse_sign_mode(arg, val, NULL))\n \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n \n \treturn 0;\n@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\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_RESIGN_IF_INVALID:\n+\t\t\tdie(_(\"'re-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,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)\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_RESIGN_IF_INVALID:\n+\t\t\t\tdie(_(\"'re-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}\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b8a7757cfd..f6bd8556f5 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -190,6 +190,7 @@ static const char *global_prefix;\n \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 \n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n@@ -2836,10 +2837,11 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n \tstrbuf_addbuf(new_data, msg);\n }\n \n-static void handle_strip_if_invalid(struct strbuf *new_data,\n-\t\t\t\t    struct signature_data *sig_sha1,\n-\t\t\t\t    struct signature_data *sig_sha256,\n-\t\t\t\t    struct strbuf *msg)\n+static void handle_signature_if_invalid(struct strbuf *new_data,\n+\t\t\t\t\tstruct signature_data *sig_sha1,\n+\t\t\t\t\tstruct signature_data *sig_sha256,\n+\t\t\t\t\tstruct strbuf *msg,\n+\t\t\t\t\tenum sign_mode mode)\n {\n \tstruct strbuf tmp_buf = STRBUF_INIT;\n \tstruct signature_check signature_check = { 0 };\n@@ -2856,15 +2858,52 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n \t\tconst char *subject;\n \t\tint subject_len = find_commit_subject(msg->buf, &subject);\n \n-\t\tif (subject_len > 100)\n-\t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n-\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n-\t\telse if (subject_len > 0)\n-\t\t\twarning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n-\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n-\t\telse\n-\t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n-\t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tif (mode == SIGN_STRIP_IF_INVALID) {\n+\t\t\tif (subject_len > 100)\n+\t\t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n+\t\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n+\t\t\telse if (subject_len > 0)\n+\t\t\t\twarning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n+\t\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n+\t\t\telse\n+\t\t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n+\t\t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\t} else if (mode == SIGN_RESIGN_IF_INVALID) {\n+\t\t\tstruct strbuf signature = STRBUF_INIT;\n+\t\t\tstruct strbuf payload = STRBUF_INIT;\n+\n+\t\t\tif (subject_len > 100)\n+\t\t\t\twarning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n+\t\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n+\t\t\telse if (subject_len > 0)\n+\t\t\t\twarning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n+\t\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n+\t\t\telse\n+\t\t\t\twarning(_(\"re-signing invalid signature for commit\\n\"\n+\t\t\t\t\t  \"  allegedly by %s\"), signer);\n+\n+\t\t\t/*\n+\t\t\t * NEEDSWORK: To properly support interoperability mode\n+\t\t\t * when re-signing commit signatures, the commit buffer\n+\t\t\t * must be provided in both the repository and\n+\t\t\t * compatability object formats. As currently\n+\t\t\t * implemented, only the repository object format is\n+\t\t\t * considered meaning compatability signatures cannot be\n+\t\t\t * generated. Thus, attempting to re-sign commit\n+\t\t\t * signatures in interoperability mode is currently\n+\t\t\t * unsupported.\n+\t\t\t */\n+\t\t\tif (the_repository->compat_hash_algo)\n+\t\t\t\tdie(_(\"re-signing signatures in interoperability mode is unsupported\"));\n+\n+\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n+\t\t\tif (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))\n+\t\t\t\tdie(_(\"failed to sign commit object\"));\n+\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n+\n+\t\t\tstrbuf_release(&signature);\n+\t\t\tstrbuf_release(&payload);\n+\t\t}\n \n \t\tfinalize_commit_buffer(new_data, NULL, NULL, msg);\n \t} else {\n@@ -2927,6 +2966,7 @@ static void parse_new_commit(const char *arg)\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n \t\tcase SIGN_STRIP_IF_INVALID:\n+\t\tcase SIGN_RESIGN_IF_INVALID:\n \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n@@ -3011,9 +3051,11 @@ static void parse_new_commit(const char *arg)\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n \n-\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n+\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n+\t     signed_commit_mode == SIGN_RESIGN_IF_INVALID) &&\n \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n-\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n+\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n+\t\t\t\t\t    &msg, signed_commit_mode);\n \telse\n \t\tfinalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);\n \n@@ -3060,6 +3102,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\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_RESIGN_IF_INVALID:\n+\t\tdie(_(\"'re-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@@ -3649,10 +3694,10 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_commit_mode))\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))\n+\t\tif (parse_sign_mode(option, &signed_tag_mode, NULL))\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/gpg-interface.c b/gpg-interface.c\nindex a72fa35061..e028984546 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1155,21 +1155,28 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \treturn ret;\n }\n \n-int parse_sign_mode(const char *arg, enum sign_mode *mode)\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)\n {\n-\tif (!strcmp(arg, \"abort\"))\n+\tif (!strcmp(arg, \"abort\")) {\n \t\t*mode = SIGN_ABORT;\n-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n+\t} else if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\")) {\n \t\t*mode = SIGN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n+\t} else if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\")) {\n \t\t*mode = SIGN_WARN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-strip\"))\n+\t} else if (!strcmp(arg, \"warn-strip\")) {\n \t\t*mode = SIGN_WARN_STRIP;\n-\telse if (!strcmp(arg, \"strip\"))\n+\t} else if (!strcmp(arg, \"strip\")) {\n \t\t*mode = SIGN_STRIP;\n-\telse if (!strcmp(arg, \"strip-if-invalid\"))\n+\t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n \t\t*mode = SIGN_STRIP_IF_INVALID;\n-\telse\n+\t} else if (!strcmp(arg, \"re-sign-if-invalid\")) {\n+\t\t*mode = SIGN_RESIGN_IF_INVALID;\n+\t} else if (skip_prefix(arg, \"re-sign-if-invalid=\", &arg)) {\n+\t\t*mode = SIGN_RESIGN_IF_INVALID;\n+\t\tif (keyid)\n+\t\t\t*keyid = arg;\n+\t} else {\n \t\treturn -1;\n+\t}\n \treturn 0;\n }\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex a32741aeda..8f1fad43e9 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -119,12 +119,15 @@ enum sign_mode {\n \tSIGN_WARN_STRIP,\n \tSIGN_STRIP,\n \tSIGN_STRIP_IF_INVALID,\n+\tSIGN_RESIGN_IF_INVALID,\n };\n \n /*\n  * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n- * otherwise.\n+ * otherwise. If the parsed mode is SIGN_RESIGN_IF_INVALID and GPG key provided\n+ * in the arguments in the form `re-sign-if-invalid=<keyid>`, the key-ID is\n+ * parsed into `char **keyid`.\n  */\n-int parse_sign_mode(const char *arg, enum sign_mode *mode);\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);\n \n #endif\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nindex 022dae02e4..2a3f04b42d 100755\n--- a/t/t9305-fast-import-signatures.sh\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -103,26 +103,85 @@ test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war\n \ttest_line_count = 2 out\n '\n \n-test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '\n-\tgit fast-export main >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n+for mode in strip-if-invalid re-sign-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+\t\tgit -C new fast-import --quiet --signed-commits=$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-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\t\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"handle signature invalidated by message change with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\n+\t\t# Change the commit message, which invalidates the signature.\n+\t\t# The commit message length should not change though, otherwise the\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\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+\t\ttest $OPENPGP_SIGNING != $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\n+\t\tif test \"$mode\" = strip-if-invalid\n+\t\tthen\n+\t\t\ttest_grep \"stripping invalid signature\" log &&\n+\t\t\ttest_grep ! -E \"^gpgsig\" actual\n+\t\telse\n+\t\t\ttest_grep \"re-signing invalid signature\" log &&\n+\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\t\tgit -C new verify-commit \"$IMPORTED\"\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success GPGSM \"keep valid X.509 signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\t\ttest $X509_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSSH \"keep valid SSH signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\t\ttest $SSH_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+done\n+\n+test_expect_success GPGSSH \"re-sign invalid commit with explicit keyid\" '\n \trm -rf new &&\n \tgit init new &&\n \n@@ -133,41 +192,22 @@ test_expect_success GPG 'strip signature invalidated by message change with --si\n \t# corresponding `data <length>` command would have to be changed too.\n \tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n \n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n+\t# Configure the target repository with an invalid default signing key.\n+\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n+\ttest_config -C new gpg.format ssh &&\n+\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git -C new fast-import --quiet \\\n+\t\t--signed-commits=re-sign-if-invalid <modified >/dev/null 2>&1 &&\n+\n+\t# Import using explicitly provided signing key.\n+\tgit -C new fast-import --quiet \\\n+\t\t--signed-commits=re-sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n \n \tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n \ttest $OPENPGP_SIGNING != $IMPORTED &&\n \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep ! -E \"^gpgsig\" actual &&\n-\ttest_grep \"stripping invalid signature\" log\n-'\n-\n-test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n-\ttest $X509_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n-\n-\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n-\ttest $SSH_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n+\tgit -C new verify-commit \"$IMPORTED\"\n '\n \n test_done\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538371","messageId":"CAP8UFD2F-81dwyOP8iMqQq2MjQ0GN-92ZyACSzbk6f7cOQAmTw@mail.gmail.com","threadId":"65062","inReplyTo":"20260306205359.1723254-3-jltobler@gmail.com","subject":"Re: [PATCH v2 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-03-10T09:01:27Z","receivedAt":"2026-03-10T09:01:39Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Mar 6, 2026 at 9:54 PM Justin Tobler <jltobler@gmail.com> wrote:\n>\n> The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\n> logic to get the default configured signing key when a key is not\n> provided and handles generating the commit signature accordingly. This\n> signing operation is not really specific to commits as any arbitrary\n> buffer can be signed. Also, in a subsequent commit, this same logic is\n> reused by git-fast-import(1) when resigning invalid commit signatures.\n\nNit: s/resigning/re-signing/\n\n> Introduce `sign_buffer_with_key()` to centralize signing key resolution\n> in gpg-interface to allow callers to reuse the same behavior without\n> duplicating logic.\n\nNit: I think it would be a bit clearer if the change was described as:\n\n- moving the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n\"gpg-interface.c\",\n- renaming it to `sign_buffer_with_key()`, and\n- exporting it (so it can later be used by both \"commit.c\" and\n\"builtin/fast-import.c\").\n\nOr did I miss something?\n"},{"id":"538373","messageId":"CAP8UFD3p84U0FhjGXNqagtDi=Cd3+QBHqGb3_ceWy-tdeLc43g@mail.gmail.com","threadId":"65062","inReplyTo":"20260306205359.1723254-4-jltobler@gmail.com","subject":"Re: [PATCH v2 3/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-03-10T09:27:14Z","receivedAt":"2026-03-10T09:27:27Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Mar 6, 2026 at 9:54 PM Justin Tobler <jltobler@gmail.com> wrote:\n\n> @@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n>                 case SIGN_STRIP_IF_INVALID:\n>                         die(_(\"'strip-if-invalid' is not a valid mode for \"\n>                               \"git fast-export with --signed-commits=<mode>\"));\n> +               case SIGN_RESIGN_IF_INVALID:\n\nEverywhere in this patch, I think \"RE_SIGN\" might be more consistent\nthan \"RESIGN\" for this name.\n\n> +                       die(_(\"'re-sign-if-invalid' is not a valid mode for \"\n> +                             \"git fast-export with --signed-commits=<mode>\"));\n\n[...]\n\n> @@ -2856,15 +2858,52 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n>                 const char *subject;\n>                 int subject_len = find_commit_subject(msg->buf, &subject);\n>\n> -               if (subject_len > 100)\n> -                       warning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n> -                                 \"  allegedly by %s\"), subject, signer);\n> -               else if (subject_len > 0)\n> -                       warning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n> -                                 \"  allegedly by %s\"), subject_len, subject, signer);\n> -               else\n> -                       warning(_(\"stripping invalid signature for commit\\n\"\n> -                                 \"  allegedly by %s\"), signer);\n> +               if (mode == SIGN_STRIP_IF_INVALID) {\n> +                       if (subject_len > 100)\n> +                               warning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n> +                                         \"  allegedly by %s\"), subject, signer);\n> +                       else if (subject_len > 0)\n> +                               warning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n> +                                         \"  allegedly by %s\"), subject_len, subject, signer);\n> +                       else\n> +                               warning(_(\"stripping invalid signature for commit\\n\"\n> +                                         \"  allegedly by %s\"), signer);\n> +               } else if (mode == SIGN_RESIGN_IF_INVALID) {\n> +                       struct strbuf signature = STRBUF_INIT;\n> +                       struct strbuf payload = STRBUF_INIT;\n> +\n> +                       if (subject_len > 100)\n> +                               warning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n> +                                         \"  allegedly by %s\"), subject, signer);\n> +                       else if (subject_len > 0)\n> +                               warning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n> +                                         \"  allegedly by %s\"), subject_len, subject, signer);\n> +                       else\n> +                               warning(_(\"re-signing invalid signature for commit\\n\"\n> +                                         \"  allegedly by %s\"), signer);\n\nMaybe a helper function could be used to avoid duplicating the warning logic.\n\n> +                       /*\n> +                        * NEEDSWORK: To properly support interoperability mode\n> +                        * when re-signing commit signatures, the commit buffer\n> +                        * must be provided in both the repository and\n> +                        * compatability object formats. As currently\n\ns/compatability/compatibility/\n\n> +                        * implemented, only the repository object format is\n> +                        * considered meaning compatability signatures cannot be\n\ns/compatability/compatibility/\n\n> +                        * generated. Thus, attempting to re-sign commit\n> +                        * signatures in interoperability mode is currently\n> +                        * unsupported.\n> +                        */\n> +                       if (the_repository->compat_hash_algo)\n> +                               die(_(\"re-signing signatures in interoperability mode is unsupported\"));\n> +\n> +                       strbuf_addstr(&payload, signature_check.payload);\n> +                       if (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))\n> +                               die(_(\"failed to sign commit object\"));\n> +                       add_header_signature(new_data, &signature, the_hash_algo);\n> +\n> +                       strbuf_release(&signature);\n> +                       strbuf_release(&payload);\n> +               }\n\nExcept for these small issues and the few nits in the previous patch,\nthis looks good to me. Thanks for working on it.\n"},{"id":"538500","messageId":"abBciWOi8D1oRJZ8@denethor","threadId":"65062","inReplyTo":"CAP8UFD2F-81dwyOP8iMqQq2MjQ0GN-92ZyACSzbk6f7cOQAmTw@mail.gmail.com","subject":"Re: [PATCH v2 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T18:04:07Z","receivedAt":"2026-03-10T18:04:09Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/10 10:01AM, Christian Couder wrote:\n> On Fri, Mar 6, 2026 at 9:54 PM Justin Tobler <jltobler@gmail.com> wrote:\n> >\n> > The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\n> > logic to get the default configured signing key when a key is not\n> > provided and handles generating the commit signature accordingly. This\n> > signing operation is not really specific to commits as any arbitrary\n> > buffer can be signed. Also, in a subsequent commit, this same logic is\n> > reused by git-fast-import(1) when resigning invalid commit signatures.\n> \n> Nit: s/resigning/re-signing/\n\nWill fix.\n\n> > Introduce `sign_buffer_with_key()` to centralize signing key resolution\n> > in gpg-interface to allow callers to reuse the same behavior without\n> > duplicating logic.\n> \n> Nit: I think it would be a bit clearer if the change was described as:\n> \n> - moving the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n> \"gpg-interface.c\",\n> - renaming it to `sign_buffer_with_key()`, and\n> - exporting it (so it can later be used by both \"commit.c\" and\n> \"builtin/fast-import.c\").\n> \n> Or did I miss something?\n\nThat's correct. I'll update the commit message in the next version to\ntry to be a bit more clear here. Thanks.\n\n-Justin\n"},{"id":"538501","messageId":"abBdNO2Izp7vrOdM@denethor","threadId":"65062","inReplyTo":"CAP8UFD3p84U0FhjGXNqagtDi=Cd3+QBHqGb3_ceWy-tdeLc43g@mail.gmail.com","subject":"Re: [PATCH v2 3/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T18:09:47Z","receivedAt":"2026-03-10T18:09:48Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/10 10:27AM, Christian Couder wrote:\n> On Fri, Mar 6, 2026 at 9:54 PM Justin Tobler <jltobler@gmail.com> wrote:\n> \n> > @@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n> >                 case SIGN_STRIP_IF_INVALID:\n> >                         die(_(\"'strip-if-invalid' is not a valid mode for \"\n> >                               \"git fast-export with --signed-commits=<mode>\"));\n> > +               case SIGN_RESIGN_IF_INVALID:\n> \n> Everywhere in this patch, I think \"RE_SIGN\" might be more consistent\n> than \"RESIGN\" for this name.\n\nThat's fair, will change.\n\n> > +                       die(_(\"'re-sign-if-invalid' is not a valid mode for \"\n> > +                             \"git fast-export with --signed-commits=<mode>\"));\n> \n> [...]\n> \n> > @@ -2856,15 +2858,52 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n> >                 const char *subject;\n> >                 int subject_len = find_commit_subject(msg->buf, &subject);\n> >\n> > -               if (subject_len > 100)\n> > -                       warning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n> > -                                 \"  allegedly by %s\"), subject, signer);\n> > -               else if (subject_len > 0)\n> > -                       warning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n> > -                                 \"  allegedly by %s\"), subject_len, subject, signer);\n> > -               else\n> > -                       warning(_(\"stripping invalid signature for commit\\n\"\n> > -                                 \"  allegedly by %s\"), signer);\n> > +               if (mode == SIGN_STRIP_IF_INVALID) {\n> > +                       if (subject_len > 100)\n> > +                               warning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n> > +                                         \"  allegedly by %s\"), subject, signer);\n> > +                       else if (subject_len > 0)\n> > +                               warning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n> > +                                         \"  allegedly by %s\"), subject_len, subject, signer);\n> > +                       else\n> > +                               warning(_(\"stripping invalid signature for commit\\n\"\n> > +                                         \"  allegedly by %s\"), signer);\n> > +               } else if (mode == SIGN_RESIGN_IF_INVALID) {\n> > +                       struct strbuf signature = STRBUF_INIT;\n> > +                       struct strbuf payload = STRBUF_INIT;\n> > +\n> > +                       if (subject_len > 100)\n> > +                               warning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n> > +                                         \"  allegedly by %s\"), subject, signer);\n> > +                       else if (subject_len > 0)\n> > +                               warning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n> > +                                         \"  allegedly by %s\"), subject_len, subject, signer);\n> > +                       else\n> > +                               warning(_(\"re-signing invalid signature for commit\\n\"\n> > +                                         \"  allegedly by %s\"), signer);\n> \n> Maybe a helper function could be used to avoid duplicating the warning logic.\n\nYa, I could extract this out to a helper that prints the appropriate\nwarning. Due to being translated, I'm not quite sure if there would be a\ngood way to make the message strings more generic though. Will update in\nthe next version.\n\n-Justin\n"},{"id":"538515","messageId":"20260310201116.1130160-1-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260306205359.1723254-1-jltobler@gmail.com","subject":"[PATCH v3 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T20:11:13Z","receivedAt":"2026-03-10T20:11:23Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nWith c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17), it became possible to remove\ninvalid signatures from commits via git-fast-import(1) while maintaining\nvalid commit signatures. Building upon this functionality, a user may\nwant to re-sign these invalid commit signatures. This series introduces\nthe `re-sign-if-invalid` mode to do so accordingly.\n\nThe newly added mode in this series currently ignores\n`extensions.compatObjectFormat` when generating the new signatures. From\nmy understanding, to generate the compatibility structure would also\nrequire us to reconstruct the compatibility object for the object being\nsigned. I think this would be possible to do, but would require getting\nthe mapped OIDs for the commit parents and tree. I'm not completely sure\nof a good way to go about this yet though. I'm also not completely\ncertain if this is something that should be addressed as part of this\nseries, or could be done later down the road. So for now I've opted to\ndelay its implementation. I'm open going down the other route if that is\npreferred though.\n\nThe first commit is a simple cleanup for something I noticed while\nreading though commit signing code. The second commit actually\nintroduces the new `--signed-commits` mode.\n\nChanges since V2:\n- Adapted commit message in second patch to improve clarity.\n- Fixed typos.\n- Renamed SIGN_RESIGN_IF_INVALID to SIGN_RE_SIGN_IF_INVALID.\n- Created separate helper function to handle printing invalid signature\n  warnings.\n\nChanges since V1:\n- Improved commit messages and comments to better explain why\n  interoperability mode is not currently supported.\n- Clarified documentation for re-sign-if-invalid mode.\n- Renamed `handle_invalid_signature()` to `handle_signature_if_invalid()`.\n- Added warning messages specific to commit resigning.\n- Fixed some small typos.\n- Added support for explicitly specifying the signing key ID via\n  `--signed-commits=re-sign-if-invalid[=<keyid>]` similar to how it can\n  specified in git-commit(1).\n- We now die() as unsupported when attempting to re-sign an invalid\n  commit signature in interoperability mode.\n- We now die() when failing to re-sign a commit.\n\nThanks,\n-Justin\n\nJustin Tobler (3):\n  commit: remove unused forward declaration\n  gpg-interface: introduce sign_buffer_with_key()\n  fast-import: add mode to re-sign invalid commit signatures\n\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 101 ++++++++++++++++-----\n commit.c                           |  16 +---\n commit.h                           |   2 -\n gpg-interface.c                    |  36 ++++++--\n gpg-interface.h                    |  14 ++-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 8 files changed, 222 insertions(+), 99 deletions(-)\n\nRange-diff against v2:\n1:  0d00b72ee0 = 1:  0d00b72ee0 commit: remove unused forward declaration\n2:  499025532c ! 2:  0b0a06347d gpg-interface: introduce sign_buffer_with_key()\n    @@ Commit message\n         provided and handles generating the commit signature accordingly. This\n         signing operation is not really specific to commits as any arbitrary\n         buffer can be signed. Also, in a subsequent commit, this same logic is\n    -    reused by git-fast-import(1) when resigning invalid commit signatures.\n    -    Introduce `sign_buffer_with_key()` to centralize signing key resolution\n    -    in gpg-interface to allow callers to reuse the same behavior without\n    -    duplicating logic.\n    +    reused by git-fast-import(1) when re-signing invalid commit signatures.\n    +\n    +    Move the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n    +    \"gpg-interface.c\" and rename it to `sign_buffer_with_key()`. Also export\n    +    this function so it can be used by \"commit.c\" and\n    +    \"builtin/fast-import.c\" in the subsequent commit.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n3:  bea1a42eb9 ! 3:  57a27ccc61 fast-import: add mode to re-sign invalid commit signatures\n    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\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_RESIGN_IF_INVALID:\n    ++\t\tcase SIGN_RE_SIGN_IF_INVALID:\n     +\t\t\tdie(_(\"'re-sign-if-invalid' is not a valid mode for \"\n     +\t\t\t      \"git fast-export with --signed-commits=<mode>\"));\n      \t\tdefault:\n    @@ builtin/fast-export.c: static void handle_tag(const char *name, struct tag *tag)\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_RESIGN_IF_INVALID:\n    ++\t\t\tcase SIGN_RE_SIGN_IF_INVALID:\n     +\t\t\t\tdie(_(\"'re-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    @@ builtin/fast-import.c: static void finalize_commit_buffer(struct strbuf *new_dat\n     -\t\t\t\t    struct signature_data *sig_sha1,\n     -\t\t\t\t    struct signature_data *sig_sha256,\n     -\t\t\t\t    struct strbuf *msg)\n    ++static void warn_invalid_signature(struct signature_check *check,\n    ++\t\t\t\t   const char *msg, enum sign_mode mode)\n    + {\n    +-\tstruct strbuf tmp_buf = STRBUF_INIT;\n    +-\tstruct signature_check signature_check = { 0 };\n    +-\tint ret;\n    +-\n    +-\t/* Check signature in a temporary commit buffer */\n    +-\tstrbuf_addbuf(&tmp_buf, new_data);\n    +-\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n    +-\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n    +-\n    +-\tif (ret) {\n    +-\t\tconst char *signer = signature_check.signer ?\n    +-\t\t\tsignature_check.signer : _(\"unknown\");\n    +-\t\tconst char *subject;\n    +-\t\tint subject_len = find_commit_subject(msg->buf, &subject);\n    ++\tconst char *signer = check->signer ? check->signer : _(\"unknown\");\n    ++\tconst char *subject;\n    ++\tint subject_len = find_commit_subject(msg, &subject);\n    + \n    ++\tswitch (mode) {\n    ++\tcase SIGN_STRIP_IF_INVALID:\n    + \t\tif (subject_len > 100)\n    + \t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n    + \t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    +@@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_data,\n    + \t\telse\n    + \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n    + \t\t\t\t  \"  allegedly by %s\"), signer);\n    ++\t\tbreak;\n    ++\tcase SIGN_RE_SIGN_IF_INVALID:\n    ++\t\tif (subject_len > 100)\n    ++\t\t\twarning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n    ++\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    ++\t\telse if (subject_len > 0)\n    ++\t\t\twarning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n    ++\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n    ++\t\telse\n    ++\t\t\twarning(_(\"re-signing invalid signature for commit\\n\"\n    ++\t\t\t\t  \"  allegedly by %s\"), signer);\n    ++\t\tbreak;\n    ++\tdefault:\n    ++\t\tBUG(\"unsupported signing mode\");\n    ++\t}\n    ++}\n    ++\n     +static void handle_signature_if_invalid(struct strbuf *new_data,\n     +\t\t\t\t\tstruct signature_data *sig_sha1,\n     +\t\t\t\t\tstruct signature_data *sig_sha256,\n     +\t\t\t\t\tstruct strbuf *msg,\n     +\t\t\t\t\tenum sign_mode mode)\n    - {\n    - \tstruct strbuf tmp_buf = STRBUF_INIT;\n    - \tstruct signature_check signature_check = { 0 };\n    -@@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_data,\n    - \t\tconst char *subject;\n    - \t\tint subject_len = find_commit_subject(msg->buf, &subject);\n    - \n    --\t\tif (subject_len > 100)\n    --\t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n    --\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    --\t\telse if (subject_len > 0)\n    --\t\t\twarning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n    --\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n    --\t\telse\n    --\t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n    --\t\t\t\t  \"  allegedly by %s\"), signer);\n    -+\t\tif (mode == SIGN_STRIP_IF_INVALID) {\n    -+\t\t\tif (subject_len > 100)\n    -+\t\t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n    -+\t\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    -+\t\t\telse if (subject_len > 0)\n    -+\t\t\t\twarning(_(\"stripping invalid signature for commit '%.*s'\\n\"\n    -+\t\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n    -+\t\t\telse\n    -+\t\t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n    -+\t\t\t\t\t  \"  allegedly by %s\"), signer);\n    -+\t\t} else if (mode == SIGN_RESIGN_IF_INVALID) {\n    ++{\n    ++\tstruct strbuf tmp_buf = STRBUF_INIT;\n    ++\tstruct signature_check signature_check = { 0 };\n    ++\tint ret;\n    ++\n    ++\t/* Check signature in a temporary commit buffer */\n    ++\tstrbuf_addbuf(&tmp_buf, new_data);\n    ++\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n    ++\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n    ++\n    ++\tif (ret) {\n    ++\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n    ++\n    ++\t\tif (mode == SIGN_RE_SIGN_IF_INVALID) {\n     +\t\t\tstruct strbuf signature = STRBUF_INIT;\n     +\t\t\tstruct strbuf payload = STRBUF_INIT;\n     +\n    -+\t\t\tif (subject_len > 100)\n    -+\t\t\t\twarning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n    -+\t\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n    -+\t\t\telse if (subject_len > 0)\n    -+\t\t\t\twarning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n    -+\t\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n    -+\t\t\telse\n    -+\t\t\t\twarning(_(\"re-signing invalid signature for commit\\n\"\n    -+\t\t\t\t\t  \"  allegedly by %s\"), signer);\n    -+\n     +\t\t\t/*\n     +\t\t\t * NEEDSWORK: To properly support interoperability mode\n     +\t\t\t * when re-signing commit signatures, the commit buffer\n     +\t\t\t * must be provided in both the repository and\n    -+\t\t\t * compatability object formats. As currently\n    ++\t\t\t * compatibility object formats. As currently\n     +\t\t\t * implemented, only the repository object format is\n    -+\t\t\t * considered meaning compatability signatures cannot be\n    ++\t\t\t * considered meaning compatibility signatures cannot be\n     +\t\t\t * generated. Thus, attempting to re-sign commit\n     +\t\t\t * signatures in interoperability mode is currently\n     +\t\t\t * unsupported.\n    @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n      \t\t\t/* fallthru */\n      \t\tcase SIGN_VERBATIM:\n      \t\tcase SIGN_STRIP_IF_INVALID:\n    -+\t\tcase SIGN_RESIGN_IF_INVALID:\n    ++\t\tcase SIGN_RE_SIGN_IF_INVALID:\n      \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n      \t\t\tbreak;\n      \n    @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n      \n     -\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n     +\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n    -+\t     signed_commit_mode == SIGN_RESIGN_IF_INVALID) &&\n    ++\t     signed_commit_mode == SIGN_RE_SIGN_IF_INVALID) &&\n      \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n     -\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n     +\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n    @@ builtin/fast-import.c: static void handle_tag_signature(struct strbuf *msg, cons\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_RESIGN_IF_INVALID:\n    ++\tcase SIGN_RE_SIGN_IF_INVALID:\n     +\t\tdie(_(\"'re-sign-if-invalid' is not a valid mode for \"\n     +\t\t      \"git fast-import with --signed-tags=<mode>\"));\n      \tdefault:\n    @@ gpg-interface.c: static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf\n      \t\t*mode = SIGN_STRIP_IF_INVALID;\n     -\telse\n     +\t} else if (!strcmp(arg, \"re-sign-if-invalid\")) {\n    -+\t\t*mode = SIGN_RESIGN_IF_INVALID;\n    ++\t\t*mode = SIGN_RE_SIGN_IF_INVALID;\n     +\t} else if (skip_prefix(arg, \"re-sign-if-invalid=\", &arg)) {\n    -+\t\t*mode = SIGN_RESIGN_IF_INVALID;\n    ++\t\t*mode = SIGN_RE_SIGN_IF_INVALID;\n     +\t\tif (keyid)\n     +\t\t\t*keyid = arg;\n     +\t} else {\n    @@ gpg-interface.h: enum sign_mode {\n      \tSIGN_WARN_STRIP,\n      \tSIGN_STRIP,\n      \tSIGN_STRIP_IF_INVALID,\n    -+\tSIGN_RESIGN_IF_INVALID,\n    ++\tSIGN_RE_SIGN_IF_INVALID,\n      };\n      \n      /*\n       * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n     - * otherwise.\n    -+ * otherwise. If the parsed mode is SIGN_RESIGN_IF_INVALID and GPG key provided\n    ++ * otherwise. If the parsed mode is SIGN_RE_SIGN_IF_INVALID and GPG key provided\n     + * in the arguments in the form `re-sign-if-invalid=<keyid>`, the key-ID is\n     + * parsed into `char **keyid`.\n       */\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538517","messageId":"20260310201116.1130160-2-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260310201116.1130160-1-jltobler@gmail.com","subject":"[PATCH v3 1/3] commit: remove unused forward declaration","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T20:11:14Z","receivedAt":"2026-03-10T20:11:24Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n`sign_with_header()` was removed, but its forward declaration in\n\"commit.h\" was left. Remove the unused declaration.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.h | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/commit.h b/commit.h\nindex 1635de418b..f0c38cb444 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -390,8 +390,6 @@ LAST_ARG_MUST_BE_NULL\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook, const char *name, ...);\n \n-/* Sign a commit or tag buffer, storing the result in a header. */\n-int sign_with_header(struct strbuf *buf, const char *keyid);\n /* Parse the signature out of a header. */\n int parse_buffer_signed_by_header(const char *buffer,\n \t\t\t\t  unsigned long size,\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538516","messageId":"20260310201116.1130160-3-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260310201116.1130160-1-jltobler@gmail.com","subject":"[PATCH v3 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T20:11:15Z","receivedAt":"2026-03-10T20:11:25Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\nlogic to get the default configured signing key when a key is not\nprovided and handles generating the commit signature accordingly. This\nsigning operation is not really specific to commits as any arbitrary\nbuffer can be signed. Also, in a subsequent commit, this same logic is\nreused by git-fast-import(1) when re-signing invalid commit signatures.\n\nMove the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n\"gpg-interface.c\" and rename it to `sign_buffer_with_key()`. Also export\nthis function so it can be used by \"commit.c\" and\n\"builtin/fast-import.c\" in the subsequent commit.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.c        | 16 ++--------------\n gpg-interface.c | 13 +++++++++++++\n gpg-interface.h |  7 +++++++\n 3 files changed, 22 insertions(+), 14 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex d16ae73345..1677b1ef25 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1148,18 +1148,6 @@ int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi\n \treturn 0;\n }\n \n-static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)\n-{\n-\tchar *keyid_to_free = NULL;\n-\tint ret = 0;\n-\tif (!keyid || !*keyid)\n-\t\tkeyid = keyid_to_free = get_signing_key();\n-\tif (sign_buffer(buf, sig, keyid))\n-\t\tret = -1;\n-\tfree(keyid_to_free);\n-\treturn ret;\n-}\n-\n int parse_signed_commit(const struct commit *commit,\n \t\t\tstruct strbuf *payload, struct strbuf *signature,\n \t\t\tconst struct git_hash_algo *algop)\n@@ -1737,7 +1725,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\toidcpy(&parent_buf[i++], &p->item->object.oid);\n \n \twrite_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);\n-\tif (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {\n+\tif (sign_commit && sign_buffer_with_key(&buffer, &sig, sign_commit)) {\n \t\tresult = -1;\n \t\tgoto out;\n \t}\n@@ -1769,7 +1757,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\tfree_commit_extra_headers(compat_extra);\n \t\tfree(mapped_parents);\n \n-\t\tif (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {\n+\t\tif (sign_commit && sign_buffer_with_key(&compat_buffer, &compat_sig, sign_commit)) {\n \t\t\tresult = -1;\n \t\t\tgoto out;\n \t\t}\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 87fb6605fb..a72fa35061 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -980,6 +980,19 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t const char *signing_key)\n+{\n+\tchar *keyid_to_free = NULL;\n+\tint ret = 0;\n+\tif (!signing_key || !*signing_key)\n+\t\tsigning_key = keyid_to_free = get_signing_key();\n+\tif (sign_buffer(buffer, signature, signing_key))\n+\t\tret = -1;\n+\tfree(keyid_to_free);\n+\treturn ret;\n+}\n+\n /*\n  * Strip CR from the line endings, in case we are on Windows.\n  * NEEDSWORK: make it trim only CRs before LFs and rename\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 789d1ffac4..a32741aeda 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -83,6 +83,13 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n \t\tconst char *signing_key);\n \n+/*\n+ * Similar to `sign_buffer()`, but uses the default configured signing key as\n+ * returned by `get_signing_key()` when the provided \"signing_key\" is NULL or\n+ * empty. Returns 0 on success, non-zero on failure.\n+ */\n+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t const char *signing_key);\n \n /*\n  * Returns corresponding string in lowercase for a given member of\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538518","messageId":"20260310201116.1130160-4-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260310201116.1130160-1-jltobler@gmail.com","subject":"[PATCH v3 3/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T20:11:16Z","receivedAt":"2026-03-10T20:11:26Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"With git-fast-import(1), handling of signed commits is controlled via\nthe `--signed-commits=<mode>` option. When an invalid signature is\nencountered, a user may want the option to re-sign the commit as opposed\nto just stripping the signature. To facilitate this, introduce a\n\"re-sign-if-invalid\" mode for the `--signed-commits` option. Optionally,\na key ID may be explicitly provided in the form\n`re-sign-if-invalid[=<keyid>]` to specify which signing key should be\nused when re-signing invalid commit signatures.\n\nNote that to properly support interoperability mode when re-signing\ncommit signatures, the commit buffer must be created in both the\nrepository and compatability object formats to generate the appropriate\nsignatures accordingly. As currently implemented, the commit buffer for\nthe compatability object format is not reconstructed and thus re-signing\ncommits in interoperability mode is not yet supported. Support may be\nadded in the future.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 101 ++++++++++++++++-----\n gpg-interface.c                    |  23 +++--\n gpg-interface.h                    |   7 +-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 6 files changed, 200 insertions(+), 83 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 479c4081da..08f7d5d89a 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -86,6 +86,10 @@ already trusted to run their own code.\n * `strip-if-invalid` will check signatures and, if they are invalid,\n   will strip them and display a warning. The validation is performed\n   in the same way as linkgit:git-verify-commit[1] does it.\n+* `re-sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\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 re-signing; otherwise the configured default signing key is used.\n \n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 0c5d2386d8..2067613267 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -64,7 +64,7 @@ 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))\n+\tif (parse_sign_mode(arg, val, NULL))\n \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n \n \treturn 0;\n@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\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_RE_SIGN_IF_INVALID:\n+\t\t\tdie(_(\"'re-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,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)\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_RE_SIGN_IF_INVALID:\n+\t\t\t\tdie(_(\"'re-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}\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b8a7757cfd..472d9ab712 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -190,6 +190,7 @@ static const char *global_prefix;\n \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 \n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n@@ -2836,26 +2837,15 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n \tstrbuf_addbuf(new_data, msg);\n }\n \n-static void handle_strip_if_invalid(struct strbuf *new_data,\n-\t\t\t\t    struct signature_data *sig_sha1,\n-\t\t\t\t    struct signature_data *sig_sha256,\n-\t\t\t\t    struct strbuf *msg)\n+static void warn_invalid_signature(struct signature_check *check,\n+\t\t\t\t   const char *msg, enum sign_mode mode)\n {\n-\tstruct strbuf tmp_buf = STRBUF_INIT;\n-\tstruct signature_check signature_check = { 0 };\n-\tint ret;\n-\n-\t/* Check signature in a temporary commit buffer */\n-\tstrbuf_addbuf(&tmp_buf, new_data);\n-\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n-\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n-\n-\tif (ret) {\n-\t\tconst char *signer = signature_check.signer ?\n-\t\t\tsignature_check.signer : _(\"unknown\");\n-\t\tconst char *subject;\n-\t\tint subject_len = find_commit_subject(msg->buf, &subject);\n+\tconst char *signer = check->signer ? check->signer : _(\"unknown\");\n+\tconst char *subject;\n+\tint subject_len = find_commit_subject(msg, &subject);\n \n+\tswitch (mode) {\n+\tcase SIGN_STRIP_IF_INVALID:\n \t\tif (subject_len > 100)\n \t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n \t\t\t\t  \"  allegedly by %s\"), subject, signer);\n@@ -2865,6 +2855,67 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n \t\telse\n \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n \t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tcase SIGN_RE_SIGN_IF_INVALID:\n+\t\tif (subject_len > 100)\n+\t\t\twarning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n+\t\telse if (subject_len > 0)\n+\t\t\twarning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n+\t\telse\n+\t\t\twarning(_(\"re-signing invalid signature for commit\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unsupported signing mode\");\n+\t}\n+}\n+\n+static void handle_signature_if_invalid(struct strbuf *new_data,\n+\t\t\t\t\tstruct signature_data *sig_sha1,\n+\t\t\t\t\tstruct signature_data *sig_sha256,\n+\t\t\t\t\tstruct strbuf *msg,\n+\t\t\t\t\tenum sign_mode mode)\n+{\n+\tstruct strbuf tmp_buf = STRBUF_INIT;\n+\tstruct signature_check signature_check = { 0 };\n+\tint ret;\n+\n+\t/* Check signature in a temporary commit buffer */\n+\tstrbuf_addbuf(&tmp_buf, new_data);\n+\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n+\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n+\n+\tif (ret) {\n+\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n+\n+\t\tif (mode == SIGN_RE_SIGN_IF_INVALID) {\n+\t\t\tstruct strbuf signature = STRBUF_INIT;\n+\t\t\tstruct strbuf payload = STRBUF_INIT;\n+\n+\t\t\t/*\n+\t\t\t * NEEDSWORK: To properly support interoperability mode\n+\t\t\t * when re-signing commit signatures, the commit buffer\n+\t\t\t * must be provided in both the repository and\n+\t\t\t * compatibility object formats. As currently\n+\t\t\t * implemented, only the repository object format is\n+\t\t\t * considered meaning compatibility signatures cannot be\n+\t\t\t * generated. Thus, attempting to re-sign commit\n+\t\t\t * signatures in interoperability mode is currently\n+\t\t\t * unsupported.\n+\t\t\t */\n+\t\t\tif (the_repository->compat_hash_algo)\n+\t\t\t\tdie(_(\"re-signing signatures in interoperability mode is unsupported\"));\n+\n+\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n+\t\t\tif (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))\n+\t\t\t\tdie(_(\"failed to sign commit object\"));\n+\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n+\n+\t\t\tstrbuf_release(&signature);\n+\t\t\tstrbuf_release(&payload);\n+\t\t}\n \n \t\tfinalize_commit_buffer(new_data, NULL, NULL, msg);\n \t} else {\n@@ -2927,6 +2978,7 @@ static void parse_new_commit(const char *arg)\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n \t\tcase SIGN_STRIP_IF_INVALID:\n+\t\tcase SIGN_RE_SIGN_IF_INVALID:\n \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n@@ -3011,9 +3063,11 @@ static void parse_new_commit(const char *arg)\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n \n-\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n+\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n+\t     signed_commit_mode == SIGN_RE_SIGN_IF_INVALID) &&\n \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n-\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n+\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n+\t\t\t\t\t    &msg, signed_commit_mode);\n \telse\n \t\tfinalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);\n \n@@ -3060,6 +3114,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\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_RE_SIGN_IF_INVALID:\n+\t\tdie(_(\"'re-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@@ -3649,10 +3706,10 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_commit_mode))\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))\n+\t\tif (parse_sign_mode(option, &signed_tag_mode, NULL))\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/gpg-interface.c b/gpg-interface.c\nindex a72fa35061..2570b641f2 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1155,21 +1155,28 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \treturn ret;\n }\n \n-int parse_sign_mode(const char *arg, enum sign_mode *mode)\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)\n {\n-\tif (!strcmp(arg, \"abort\"))\n+\tif (!strcmp(arg, \"abort\")) {\n \t\t*mode = SIGN_ABORT;\n-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n+\t} else if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\")) {\n \t\t*mode = SIGN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n+\t} else if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\")) {\n \t\t*mode = SIGN_WARN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-strip\"))\n+\t} else if (!strcmp(arg, \"warn-strip\")) {\n \t\t*mode = SIGN_WARN_STRIP;\n-\telse if (!strcmp(arg, \"strip\"))\n+\t} else if (!strcmp(arg, \"strip\")) {\n \t\t*mode = SIGN_STRIP;\n-\telse if (!strcmp(arg, \"strip-if-invalid\"))\n+\t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n \t\t*mode = SIGN_STRIP_IF_INVALID;\n-\telse\n+\t} else if (!strcmp(arg, \"re-sign-if-invalid\")) {\n+\t\t*mode = SIGN_RE_SIGN_IF_INVALID;\n+\t} else if (skip_prefix(arg, \"re-sign-if-invalid=\", &arg)) {\n+\t\t*mode = SIGN_RE_SIGN_IF_INVALID;\n+\t\tif (keyid)\n+\t\t\t*keyid = arg;\n+\t} else {\n \t\treturn -1;\n+\t}\n \treturn 0;\n }\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex a32741aeda..e9f451f366 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -119,12 +119,15 @@ enum sign_mode {\n \tSIGN_WARN_STRIP,\n \tSIGN_STRIP,\n \tSIGN_STRIP_IF_INVALID,\n+\tSIGN_RE_SIGN_IF_INVALID,\n };\n \n /*\n  * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n- * otherwise.\n+ * otherwise. If the parsed mode is SIGN_RE_SIGN_IF_INVALID and GPG key provided\n+ * in the arguments in the form `re-sign-if-invalid=<keyid>`, the key-ID is\n+ * parsed into `char **keyid`.\n  */\n-int parse_sign_mode(const char *arg, enum sign_mode *mode);\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);\n \n #endif\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nindex 022dae02e4..2a3f04b42d 100755\n--- a/t/t9305-fast-import-signatures.sh\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -103,26 +103,85 @@ test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war\n \ttest_line_count = 2 out\n '\n \n-test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '\n-\tgit fast-export main >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n+for mode in strip-if-invalid re-sign-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+\t\tgit -C new fast-import --quiet --signed-commits=$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-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\t\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"handle signature invalidated by message change with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\n+\t\t# Change the commit message, which invalidates the signature.\n+\t\t# The commit message length should not change though, otherwise the\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\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+\t\ttest $OPENPGP_SIGNING != $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\n+\t\tif test \"$mode\" = strip-if-invalid\n+\t\tthen\n+\t\t\ttest_grep \"stripping invalid signature\" log &&\n+\t\t\ttest_grep ! -E \"^gpgsig\" actual\n+\t\telse\n+\t\t\ttest_grep \"re-signing invalid signature\" log &&\n+\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\t\tgit -C new verify-commit \"$IMPORTED\"\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success GPGSM \"keep valid X.509 signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\t\ttest $X509_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSSH \"keep valid SSH signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\t\ttest $SSH_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+done\n+\n+test_expect_success GPGSSH \"re-sign invalid commit with explicit keyid\" '\n \trm -rf new &&\n \tgit init new &&\n \n@@ -133,41 +192,22 @@ test_expect_success GPG 'strip signature invalidated by message change with --si\n \t# corresponding `data <length>` command would have to be changed too.\n \tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n \n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n+\t# Configure the target repository with an invalid default signing key.\n+\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n+\ttest_config -C new gpg.format ssh &&\n+\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git -C new fast-import --quiet \\\n+\t\t--signed-commits=re-sign-if-invalid <modified >/dev/null 2>&1 &&\n+\n+\t# Import using explicitly provided signing key.\n+\tgit -C new fast-import --quiet \\\n+\t\t--signed-commits=re-sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n \n \tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n \ttest $OPENPGP_SIGNING != $IMPORTED &&\n \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep ! -E \"^gpgsig\" actual &&\n-\ttest_grep \"stripping invalid signature\" log\n-'\n-\n-test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n-\ttest $X509_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n-\n-\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n-\ttest $SSH_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n+\tgit -C new verify-commit \"$IMPORTED\"\n '\n \n test_done\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538521","messageId":"xmqqv7f3s93l.fsf@gitster.g","threadId":"65062","inReplyTo":"20260310201116.1130160-1-jltobler@gmail.com","subject":"Re: [PATCH v3 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-10T20:49:50Z","receivedAt":"2026-03-10T20:49:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> With c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n> --signed-commits=<mode>, 2025-11-17), it became possible to remove\n> invalid signatures from commits via git-fast-import(1) while maintaining\n> valid commit signatures. Building upon this functionality, a user may\n> want to re-sign these invalid commit signatures. This series introduces\n> the `re-sign-if-invalid` mode to do so accordingly.\n\nI know that this \"re-sign\" used to be \"resign\", and the update is\nindeed a replacement, but I wonder if we can just say \"sign\"?\n\nWhen we see a signature on an object we are rewriting, we either\n\"strip\" it, or we \"sign\" it (afresh).  It is not like we are\nretaining the old signature, and signing on top of it.  We are\ndiscarding the old one so there is no difference from signing the\nobject that never had a signature, no?\n\n"},{"id":"538526","messageId":"abCFKEHxu7OZr9bm@denethor","threadId":"65062","inReplyTo":"xmqqv7f3s93l.fsf@gitster.g","subject":"Re: [PATCH v3 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T21:06:23Z","receivedAt":"2026-03-10T21:06:27Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/10 01:49PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > With c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n> > --signed-commits=<mode>, 2025-11-17), it became possible to remove\n> > invalid signatures from commits via git-fast-import(1) while maintaining\n> > valid commit signatures. Building upon this functionality, a user may\n> > want to re-sign these invalid commit signatures. This series introduces\n> > the `re-sign-if-invalid` mode to do so accordingly.\n> \n> I know that this \"re-sign\" used to be \"resign\", and the update is\n> indeed a replacement, but I wonder if we can just say \"sign\"?\n> \n> When we see a signature on an object we are rewriting, we either\n> \"strip\" it, or we \"sign\" it (afresh).  It is not like we are\n> retaining the old signature, and signing on top of it.  We are\n> discarding the old one so there is no difference from signing the\n> object that never had a signature, no?\n\nFrom my perspective, \"re-sign\" implies that the signature was previously\nsigned, but we are now going to sign it again. Indeed, the resulting\ncommit signing is functionally the same as if the object never had a\nprevious signature though. Also, \"if-invalid\" already implies that the\nobject is signed, but its signature is invalid. So it could be argued\nthat \"re-sign\" is already redundant.\n\nUltimately, I don't feel super strongly, but I can send another version\nthat changes this option to \"sign-if-invalid\". It's probably a bit\nsimpler this way too. I guess the enum value would need to be changed to\n\"SIGN_SIGN_IF_INVALID\"? Or maybe just \"SIGN_IF_INVALID\"?\n\nThanks,\n-Justin\n"},{"id":"538530","messageId":"xmqqqzprs7o3.fsf@gitster.g","threadId":"65062","inReplyTo":"abCFKEHxu7OZr9bm@denethor","subject":"Re: [PATCH v3 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-10T21:20:44Z","receivedAt":"2026-03-10T21:20:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> From my perspective, \"re-sign\" implies that the signature was previously\n> signed, but we are now going to sign it again. Indeed, the resulting\n> commit signing is functionally the same as if the object never had a\n> previous signature though. Also, \"if-invalid\" already implies that the\n> object is signed, but its signature is invalid. So it could be argued\n> that \"re-sign\" is already redundant.\n\nYup.  if-invalid part indeed was why I thought \"re-\" was redundant.\n\nAlso, if a project is redoing its history with such a bulk\noperation, I wonder if it _still_ makes sense to tie this re-signing\nto the --signed-{tags,commits} option.  Adding signature to commits\nthat were not signed is not covered well with the\n\"--signed-commits=<mode>\" option.\n\nA project may have required that all commits and tags to be signed,\nin which case \"--signed-*=sign-if-invalid\" would create a new\nhistory with everything freshly signed, but if the original history\nhas signed and unsigned commits, and if they want to sign all the\nobjects while rewriting their history, they may find it more handy\nif we let them do --signed-commits=strip-if-invalid --sign-commits\ni.e., drop the invalid ones and make sure all commits are signed.\n\n"},{"id":"538532","messageId":"abCTTaYCQIub_xjW@denethor","threadId":"65062","inReplyTo":"xmqqqzprs7o3.fsf@gitster.g","subject":"Re: [PATCH v3 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T22:13:34Z","receivedAt":"2026-03-10T22:13:39Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/10 02:20PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > From my perspective, \"re-sign\" implies that the signature was previously\n> > signed, but we are now going to sign it again. Indeed, the resulting\n> > commit signing is functionally the same as if the object never had a\n> > previous signature though. Also, \"if-invalid\" already implies that the\n> > object is signed, but its signature is invalid. So it could be argued\n> > that \"re-sign\" is already redundant.\n> \n> Yup.  if-invalid part indeed was why I thought \"re-\" was redundant.\n> \n> Also, if a project is redoing its history with such a bulk\n> operation, I wonder if it _still_ makes sense to tie this re-signing\n> to the --signed-{tags,commits} option.  Adding signature to commits\n> that were not signed is not covered well with the\n> \"--signed-commits=<mode>\" option.\n\nYa, the --signed-{tags,commits} option is really only intended to\nspecify how already signed objects should be handled. Adding a mode to\nsign unsigned objects likely wouldn't fit well. I do think this\n\"re-signing\" mode still makes sense though since it is limited to\nthe subset of objects that were previously signed and the signature\ninvalid.\n\n> A project may have required that all commits and tags to be signed,\n> in which case \"--signed-*=sign-if-invalid\" would create a new\n> history with everything freshly signed, but if the original history\n> has signed and unsigned commits, and if they want to sign all the\n> objects while rewriting their history, they may find it more handy\n> if we let them do --signed-commits=strip-if-invalid --sign-commits\n> i.e., drop the invalid ones and make sure all commits are signed.\n\nThis certainly seems like a reasonable use case, but if we want to\nsupport leaving previously unsigned objects unsigned too,\n`--signed-commits=strip-if-invalid --signed-commits` wouldn't be\ngranular enough. My thinking is that users may want such targeted object\nre-signing when bulk rewriting history via tools such as\ngit-filter-repo. I do think that it could make sense to still add a\nseparate `--signed-commits` option in the future though that targets the\nremaining unsigned objects.\n\nThanks,\n-Justin\n"},{"id":"538534","messageId":"xmqqh5qns4h9.fsf@gitster.g","threadId":"65062","inReplyTo":"20260310201116.1130160-2-jltobler@gmail.com","subject":"Re: [PATCH v3 1/3] commit: remove unused forward declaration","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-10T22:29:38Z","receivedAt":"2026-03-10T22:29:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n> `sign_with_header()` was removed, but its forward declaration in\n> \"commit.h\" was left. Remove the unused declaration.\n>\n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> ---\n>  commit.h | 2 --\n>  1 file changed, 2 deletions(-)\n>\n> diff --git a/commit.h b/commit.h\n> index 1635de418b..f0c38cb444 100644\n> --- a/commit.h\n> +++ b/commit.h\n> @@ -390,8 +390,6 @@ LAST_ARG_MUST_BE_NULL\n>  int run_commit_hook(int editor_is_used, const char *index_file,\n>  \t\t    int *invoked_hook, const char *name, ...);\n>  \n> -/* Sign a commit or tag buffer, storing the result in a header. */\n> -int sign_with_header(struct strbuf *buf, const char *keyid);\n>  /* Parse the signature out of a header. */\n>  int parse_buffer_signed_by_header(const char *buffer,\n>  \t\t\t\t  unsigned long size,\n\nNice and obvious clean-up.\n"},{"id":"538536","messageId":"xmqqcy1bs4aa.fsf@gitster.g","threadId":"65062","inReplyTo":"20260310201116.1130160-3-jltobler@gmail.com","subject":"Re: [PATCH v3 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-10T22:33:49Z","receivedAt":"2026-03-10T22:33:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\n> logic to get the default configured signing key when a key is not\n> provided and handles generating the commit signature accordingly. This\n> signing operation is not really specific to commits as any arbitrary\n> buffer can be signed. Also, in a subsequent commit, this same logic is\n> reused by git-fast-import(1) when re-signing invalid commit signatures.\n>\n> Move the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n> \"gpg-interface.c\" and rename it to `sign_buffer_with_key()`. Also export\n> this function so it can be used by \"commit.c\" and\n> \"builtin/fast-import.c\" in the subsequent commit.\n>\n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> ---\n>  commit.c        | 16 ++--------------\n>  gpg-interface.c | 13 +++++++++++++\n>  gpg-interface.h |  7 +++++++\n>  3 files changed, 22 insertions(+), 14 deletions(-)\n\nSennsible restructuring that makes the machinery easier to reuse.\nUpdated function is named more appropriately for public consumption.\n\nOverall a very welcome preparation step.\n\n> diff --git a/commit.c b/commit.c\n> index d16ae73345..1677b1ef25 100644\n> --- a/commit.c\n> +++ b/commit.c\n> @@ -1148,18 +1148,6 @@ int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi\n>  \treturn 0;\n>  }\n>  \n> -static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)\n> -{\n> -\tchar *keyid_to_free = NULL;\n> -\tint ret = 0;\n> -\tif (!keyid || !*keyid)\n> -\t\tkeyid = keyid_to_free = get_signing_key();\n> -\tif (sign_buffer(buf, sig, keyid))\n> -\t\tret = -1;\n> -\tfree(keyid_to_free);\n> -\treturn ret;\n> -}\n> -\n>  int parse_signed_commit(const struct commit *commit,\n>  \t\t\tstruct strbuf *payload, struct strbuf *signature,\n>  \t\t\tconst struct git_hash_algo *algop)\n> @@ -1737,7 +1725,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n>  \t\toidcpy(&parent_buf[i++], &p->item->object.oid);\n>  \n>  \twrite_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);\n> -\tif (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {\n> +\tif (sign_commit && sign_buffer_with_key(&buffer, &sig, sign_commit)) {\n>  \t\tresult = -1;\n>  \t\tgoto out;\n>  \t}\n> @@ -1769,7 +1757,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n>  \t\tfree_commit_extra_headers(compat_extra);\n>  \t\tfree(mapped_parents);\n>  \n> -\t\tif (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {\n> +\t\tif (sign_commit && sign_buffer_with_key(&compat_buffer, &compat_sig, sign_commit)) {\n>  \t\t\tresult = -1;\n>  \t\t\tgoto out;\n>  \t\t}\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 87fb6605fb..a72fa35061 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -980,6 +980,19 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n>  \treturn use_format->sign_buffer(buffer, signature, signing_key);\n>  }\n>  \n> +int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t const char *signing_key)\n> +{\n> +\tchar *keyid_to_free = NULL;\n> +\tint ret = 0;\n> +\tif (!signing_key || !*signing_key)\n> +\t\tsigning_key = keyid_to_free = get_signing_key();\n> +\tif (sign_buffer(buffer, signature, signing_key))\n> +\t\tret = -1;\n> +\tfree(keyid_to_free);\n> +\treturn ret;\n> +}\n> +\n>  /*\n>   * Strip CR from the line endings, in case we are on Windows.\n>   * NEEDSWORK: make it trim only CRs before LFs and rename\n> diff --git a/gpg-interface.h b/gpg-interface.h\n> index 789d1ffac4..a32741aeda 100644\n> --- a/gpg-interface.h\n> +++ b/gpg-interface.h\n> @@ -83,6 +83,13 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n>  int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n>  \t\tconst char *signing_key);\n>  \n> +/*\n> + * Similar to `sign_buffer()`, but uses the default configured signing key as\n> + * returned by `get_signing_key()` when the provided \"signing_key\" is NULL or\n> + * empty. Returns 0 on success, non-zero on failure.\n> + */\n> +int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t const char *signing_key);\n>  \n>  /*\n>   * Returns corresponding string in lowercase for a given member of\n"},{"id":"538537","messageId":"xmqq8qbzs40p.fsf@gitster.g","threadId":"65062","inReplyTo":"abCTTaYCQIub_xjW@denethor","subject":"Re: [PATCH v3 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-10T22:39:34Z","receivedAt":"2026-03-10T22:39:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> This certainly seems like a reasonable use case, but if we want to\n> support leaving previously unsigned objects unsigned too,\n> `--signed-commits=strip-if-invalid --signed-commits` wouldn't be\n> granular enough.\n\nYes, --signed-commits=(re-)sign-if-invalid is a perfect match for\nthat use case.  I am just saying that if you add the machinery\nneeded to re-sign, you would be able to reuse it to sign objects\nthat weren't signed in the first place, so that is wherea yet\nanother feature \"--sign-commits=all\" may fit.\n"},{"id":"538540","messageId":"abCgIt6X72UR2vbU@denethor","threadId":"65062","inReplyTo":"xmqq8qbzs40p.fsf@gitster.g","subject":"Re: [PATCH v3 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-10T23:03:08Z","receivedAt":"2026-03-10T23:03:12Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/10 03:39PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > This certainly seems like a reasonable use case, but if we want to\n> > support leaving previously unsigned objects unsigned too,\n> > `--signed-commits=strip-if-invalid --signed-commits` wouldn't be\n> > granular enough.\n> \n> Yes, --signed-commits=(re-)sign-if-invalid is a perfect match for\n> that use case.  I am just saying that if you add the machinery\n> needed to re-sign, you would be able to reuse it to sign objects\n> that weren't signed in the first place, so that is wherea yet\n> another feature \"--sign-commits=all\" may fit.\n\nAh ok. Ya, adding a signed-commits mode to cover signing all rewritten\nobjects could make sense. I am planning to also add an\n--abort-if-invalid mode in a followup series. I'll may explore adding\nthis other mode too.\n\nThanks,\n-Justin\n"},{"id":"538629","messageId":"20260311173147.2336432-1-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260310201116.1130160-1-jltobler@gmail.com","subject":"[PATCH v4 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-11T17:31:44Z","receivedAt":"2026-03-11T17:31:54Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nWith c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17), it became possible to remove\ninvalid signatures from commits via git-fast-import(1) while maintaining\nvalid commit signatures. Building upon this functionality, a user may\nwant to re-sign these invalid commit signatures. This series introduces\nthe `sign-if-invalid` mode to do so accordingly.\n\nThe newly added mode in this series currently ignores\n`extensions.compatObjectFormat` when generating the new signatures. From\nmy understanding, to generate the compatibility structure would also\nrequire us to reconstruct the compatibility object for the object being\nsigned. I think this would be possible to do, but would require getting\nthe mapped OIDs for the commit parents and tree. I'm not completely sure\nof a good way to go about this yet though. I'm also not completely\ncertain if this is something that should be addressed as part of this\nseries, or could be done later down the road. So for now I've opted to\ndelay its implementation. I'm open going down the other route if that is\npreferred though.\n\nThe first commit is a simple cleanup for something I noticed while\nreading though commit signing code. The second commit actually\nintroduces the new `--signed-commits` mode.\n\nChanges since V3:\n- Rename the `re-sign-if-invalid` mode to `sign-if-invalid`. The\n  \"if-invalid\" already implies the signatures was previously signed\n  making \"re-sign\" redundant.\n\nChanges since V2:\n- Adapted commit message in second patch to improve clarity.\n- Fixed typos.\n- Renamed SIGN_RESIGN_IF_INVALID to SIGN_RE_SIGN_IF_INVALID.\n- Created separate helper function to handle printing invalid signature\n  warnings.\n\nChanges since V1:\n- Improved commit messages and comments to better explain why\n  interoperability mode is not currently supported.\n- Clarified documentation for re-sign-if-invalid mode.\n- Renamed `handle_invalid_signature()` to `handle_signature_if_invalid()`.\n- Added warning messages specific to commit resigning.\n- Fixed some small typos.\n- Added support for explicitly specifying the signing key ID via\n  `--signed-commits=re-sign-if-invalid[=<keyid>]` similar to how it can\n  specified in git-commit(1).\n- We now die() as unsupported when attempting to re-sign an invalid\n  commit signature in interoperability mode.\n- We now die() when failing to re-sign a commit.\n\nThanks,\n-Justin\n\nJustin Tobler (3):\n  commit: remove unused forward declaration\n  gpg-interface: introduce sign_buffer_with_key()\n  fast-import: add mode to sign commits with invalid signatures\n\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 100 ++++++++++++++++-----\n commit.c                           |  16 +---\n commit.h                           |   2 -\n gpg-interface.c                    |  36 ++++++--\n gpg-interface.h                    |  14 ++-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 8 files changed, 221 insertions(+), 99 deletions(-)\n\nRange-diff against v3:\n1:  0d00b72ee0 = 1:  0d00b72ee0 commit: remove unused forward declaration\n2:  0b0a06347d ! 2:  87f590f1f8 gpg-interface: introduce sign_buffer_with_key()\n    @@ Commit message\n         provided and handles generating the commit signature accordingly. This\n         signing operation is not really specific to commits as any arbitrary\n         buffer can be signed. Also, in a subsequent commit, this same logic is\n    -    reused by git-fast-import(1) when re-signing invalid commit signatures.\n    +    reused by git-fast-import(1) when signing commits with invalid\n    +    signatures.\n     \n         Move the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n         \"gpg-interface.c\" and rename it to `sign_buffer_with_key()`. Also export\n3:  57a27ccc61 ! 3:  8b01ad1570 fast-import: add mode to re-sign invalid commit signatures\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    fast-import: add mode to re-sign invalid commit signatures\n    +    fast-import: add mode to sign commits with invalid signatures\n     \n         With git-fast-import(1), handling of signed commits is controlled via\n         the `--signed-commits=<mode>` option. When an invalid signature is\n    -    encountered, a user may want the option to re-sign the commit as opposed\n    -    to just stripping the signature. To facilitate this, introduce a\n    -    \"re-sign-if-invalid\" mode for the `--signed-commits` option. Optionally,\n    -    a key ID may be explicitly provided in the form\n    -    `re-sign-if-invalid[=<keyid>]` to specify which signing key should be\n    -    used when re-signing invalid commit signatures.\n    +    encountered, a user may want the option to sign the commit again as\n    +    opposed to just stripping the signature. To facilitate this, introduce a\n    +    \"sign-if-invalid\" mode for the `--signed-commits` option. Optionally, a\n    +    key ID may be explicitly provided in the form\n    +    `sign-if-invalid[=<keyid>]` to specify which signing key should be used\n    +    when signing invalid commit signatures.\n     \n    -    Note that to properly support interoperability mode when re-signing\n    -    commit signatures, the commit buffer must be created in both the\n    -    repository and compatability object formats to generate the appropriate\n    -    signatures accordingly. As currently implemented, the commit buffer for\n    -    the compatability object format is not reconstructed and thus re-signing\n    +    Note that to properly support interoperability mode when signing commit\n    +    signatures, the commit buffer must be created in both the repository and\n    +    compatability object formats to generate the appropriate signatures\n    +    accordingly. As currently implemented, the commit buffer for the\n    +    compatability object format is not reconstructed and thus signing\n         commits in interoperability mode is not yet supported. Support may be\n         added in the future.\n     \n    @@ Documentation/git-fast-import.adoc: already trusted to run their own code.\n      * `strip-if-invalid` will check signatures and, if they are invalid,\n        will strip them and display a warning. The validation is performed\n        in the same way as linkgit:git-verify-commit[1] does it.\n    -+* `re-sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\n    ++* `sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\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 re-signing; otherwise the configured default signing key is used.\n    ++  used for signing; otherwise the configured default signing key is used.\n      \n      Options for Frontends\n      ~~~~~~~~~~~~~~~~~~~~~\n    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\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_RE_SIGN_IF_INVALID:\n    -+\t\t\tdie(_(\"'re-sign-if-invalid' is not a valid mode for \"\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    @@ builtin/fast-export.c: static void handle_tag(const char *name, struct tag *tag)\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_RE_SIGN_IF_INVALID:\n    -+\t\t\t\tdie(_(\"'re-sign-if-invalid' is not a valid mode for \"\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    @@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_da\n      \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n      \t\t\t\t  \"  allegedly by %s\"), signer);\n     +\t\tbreak;\n    -+\tcase SIGN_RE_SIGN_IF_INVALID:\n    ++\tcase SIGN_SIGN_IF_INVALID:\n     +\t\tif (subject_len > 100)\n    -+\t\t\twarning(_(\"re-signing invalid signature for commit '%.100s...'\\n\"\n    ++\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n     +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n     +\t\telse if (subject_len > 0)\n    -+\t\t\twarning(_(\"re-signing invalid signature for commit '%.*s'\\n\"\n    ++\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n     +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n     +\t\telse\n    -+\t\t\twarning(_(\"re-signing invalid signature for commit\\n\"\n    ++\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n     +\t\t\t\t  \"  allegedly by %s\"), signer);\n     +\t\tbreak;\n     +\tdefault:\n    @@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_da\n     +\tif (ret) {\n     +\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n     +\n    -+\t\tif (mode == SIGN_RE_SIGN_IF_INVALID) {\n    ++\t\tif (mode == SIGN_SIGN_IF_INVALID) {\n     +\t\t\tstruct strbuf signature = STRBUF_INIT;\n     +\t\t\tstruct strbuf payload = STRBUF_INIT;\n     +\n     +\t\t\t/*\n     +\t\t\t * NEEDSWORK: To properly support interoperability mode\n    -+\t\t\t * when re-signing commit signatures, the commit buffer\n    ++\t\t\t * when signing commit signatures, the commit buffer\n     +\t\t\t * must be provided in both the repository and\n     +\t\t\t * compatibility object formats. As currently\n     +\t\t\t * implemented, only the repository object format is\n     +\t\t\t * considered meaning compatibility signatures cannot be\n    -+\t\t\t * generated. Thus, attempting to re-sign commit\n    -+\t\t\t * signatures in interoperability mode is currently\n    -+\t\t\t * unsupported.\n    ++\t\t\t * generated. Thus, attempting to sign commit signatures\n    ++\t\t\t * in interoperability mode is currently unsupported.\n     +\t\t\t */\n     +\t\t\tif (the_repository->compat_hash_algo)\n    -+\t\t\t\tdie(_(\"re-signing signatures in interoperability mode is unsupported\"));\n    ++\t\t\t\tdie(_(\"signing signatures in interoperability mode is unsupported\"));\n     +\n     +\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n     +\t\t\tif (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))\n    @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n      \t\t\t/* fallthru */\n      \t\tcase SIGN_VERBATIM:\n      \t\tcase SIGN_STRIP_IF_INVALID:\n    -+\t\tcase SIGN_RE_SIGN_IF_INVALID:\n    ++\t\tcase SIGN_SIGN_IF_INVALID:\n      \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n      \t\t\tbreak;\n      \n    @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n      \n     -\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n     +\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n    -+\t     signed_commit_mode == SIGN_RE_SIGN_IF_INVALID) &&\n    ++\t     signed_commit_mode == SIGN_SIGN_IF_INVALID) &&\n      \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n     -\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n     +\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n    @@ builtin/fast-import.c: static void handle_tag_signature(struct strbuf *msg, cons\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_RE_SIGN_IF_INVALID:\n    -+\t\tdie(_(\"'re-sign-if-invalid' is not a valid mode for \"\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    @@ gpg-interface.c: static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf\n     +\t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n      \t\t*mode = SIGN_STRIP_IF_INVALID;\n     -\telse\n    -+\t} else if (!strcmp(arg, \"re-sign-if-invalid\")) {\n    -+\t\t*mode = SIGN_RE_SIGN_IF_INVALID;\n    -+\t} else if (skip_prefix(arg, \"re-sign-if-invalid=\", &arg)) {\n    -+\t\t*mode = SIGN_RE_SIGN_IF_INVALID;\n    ++\t} else if (!strcmp(arg, \"sign-if-invalid\")) {\n    ++\t\t*mode = SIGN_SIGN_IF_INVALID;\n    ++\t} else if (skip_prefix(arg, \"sign-if-invalid=\", &arg)) {\n    ++\t\t*mode = SIGN_SIGN_IF_INVALID;\n     +\t\tif (keyid)\n     +\t\t\t*keyid = arg;\n     +\t} else {\n    @@ gpg-interface.h: enum sign_mode {\n      \tSIGN_WARN_STRIP,\n      \tSIGN_STRIP,\n      \tSIGN_STRIP_IF_INVALID,\n    -+\tSIGN_RE_SIGN_IF_INVALID,\n    ++\tSIGN_SIGN_IF_INVALID,\n      };\n      \n      /*\n       * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n     - * otherwise.\n    -+ * otherwise. If the parsed mode is SIGN_RE_SIGN_IF_INVALID and GPG key provided\n    -+ * in the arguments in the form `re-sign-if-invalid=<keyid>`, the key-ID is\n    -+ * parsed into `char **keyid`.\n    ++ * otherwise. If the parsed mode is SIGN_SIGN_IF_INVALID and GPG key provided in\n    ++ * the arguments in the form `sign-if-invalid=<keyid>`, the key-ID is parsed\n    ++ * into `char **keyid`.\n       */\n     -int parse_sign_mode(const char *arg, enum sign_mode *mode);\n     +int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     -'\n     -\n     -test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n    -+for mode in strip-if-invalid re-sign-if-invalid\n    ++for mode in strip-if-invalid sign-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    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     +\t\t\ttest_grep \"stripping invalid signature\" log &&\n     +\t\t\ttest_grep ! -E \"^gpgsig\" actual\n     +\t\telse\n    -+\t\t\ttest_grep \"re-signing invalid signature\" log &&\n    ++\t\t\ttest_grep \"signing commit with invalid signature\" log &&\n     +\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n     +\t\t\tgit -C new verify-commit \"$IMPORTED\"\n     +\t\tfi\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     +\t'\n     +done\n     +\n    -+test_expect_success GPGSSH \"re-sign invalid commit with explicit keyid\" '\n    ++test_expect_success GPGSSH \"sign invalid commit with explicit keyid\" '\n      \trm -rf new &&\n      \tgit init new &&\n      \n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip signature inva\n     +\ttest_config -C new gpg.format ssh &&\n     +\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n     +\ttest_must_fail git -C new fast-import --quiet \\\n    -+\t\t--signed-commits=re-sign-if-invalid <modified >/dev/null 2>&1 &&\n    ++\t\t--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&\n     +\n     +\t# Import using explicitly provided signing key.\n     +\tgit -C new fast-import --quiet \\\n    -+\t\t--signed-commits=re-sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n    ++\t\t--signed-commits=sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n      \n      \tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n      \ttest $OPENPGP_SIGNING != $IMPORTED &&\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538628","messageId":"20260311173147.2336432-2-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260311173147.2336432-1-jltobler@gmail.com","subject":"[PATCH v4 1/3] commit: remove unused forward declaration","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-11T17:31:45Z","receivedAt":"2026-03-11T17:31:55Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n`sign_with_header()` was removed, but its forward declaration in\n\"commit.h\" was left. Remove the unused declaration.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.h | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/commit.h b/commit.h\nindex 1635de418b..f0c38cb444 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -390,8 +390,6 @@ LAST_ARG_MUST_BE_NULL\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook, const char *name, ...);\n \n-/* Sign a commit or tag buffer, storing the result in a header. */\n-int sign_with_header(struct strbuf *buf, const char *keyid);\n /* Parse the signature out of a header. */\n int parse_buffer_signed_by_header(const char *buffer,\n \t\t\t\t  unsigned long size,\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538630","messageId":"20260311173147.2336432-3-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260311173147.2336432-1-jltobler@gmail.com","subject":"[PATCH v4 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-11T17:31:46Z","receivedAt":"2026-03-11T17:31:56Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\nlogic to get the default configured signing key when a key is not\nprovided and handles generating the commit signature accordingly. This\nsigning operation is not really specific to commits as any arbitrary\nbuffer can be signed. Also, in a subsequent commit, this same logic is\nreused by git-fast-import(1) when signing commits with invalid\nsignatures.\n\nMove the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n\"gpg-interface.c\" and rename it to `sign_buffer_with_key()`. Also export\nthis function so it can be used by \"commit.c\" and\n\"builtin/fast-import.c\" in the subsequent commit.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.c        | 16 ++--------------\n gpg-interface.c | 13 +++++++++++++\n gpg-interface.h |  7 +++++++\n 3 files changed, 22 insertions(+), 14 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex d16ae73345..1677b1ef25 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1148,18 +1148,6 @@ int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi\n \treturn 0;\n }\n \n-static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)\n-{\n-\tchar *keyid_to_free = NULL;\n-\tint ret = 0;\n-\tif (!keyid || !*keyid)\n-\t\tkeyid = keyid_to_free = get_signing_key();\n-\tif (sign_buffer(buf, sig, keyid))\n-\t\tret = -1;\n-\tfree(keyid_to_free);\n-\treturn ret;\n-}\n-\n int parse_signed_commit(const struct commit *commit,\n \t\t\tstruct strbuf *payload, struct strbuf *signature,\n \t\t\tconst struct git_hash_algo *algop)\n@@ -1737,7 +1725,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\toidcpy(&parent_buf[i++], &p->item->object.oid);\n \n \twrite_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);\n-\tif (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {\n+\tif (sign_commit && sign_buffer_with_key(&buffer, &sig, sign_commit)) {\n \t\tresult = -1;\n \t\tgoto out;\n \t}\n@@ -1769,7 +1757,7 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\tfree_commit_extra_headers(compat_extra);\n \t\tfree(mapped_parents);\n \n-\t\tif (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {\n+\t\tif (sign_commit && sign_buffer_with_key(&compat_buffer, &compat_sig, sign_commit)) {\n \t\t\tresult = -1;\n \t\t\tgoto out;\n \t\t}\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 87fb6605fb..a72fa35061 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -980,6 +980,19 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t const char *signing_key)\n+{\n+\tchar *keyid_to_free = NULL;\n+\tint ret = 0;\n+\tif (!signing_key || !*signing_key)\n+\t\tsigning_key = keyid_to_free = get_signing_key();\n+\tif (sign_buffer(buffer, signature, signing_key))\n+\t\tret = -1;\n+\tfree(keyid_to_free);\n+\treturn ret;\n+}\n+\n /*\n  * Strip CR from the line endings, in case we are on Windows.\n  * NEEDSWORK: make it trim only CRs before LFs and rename\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 789d1ffac4..a32741aeda 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -83,6 +83,13 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n \t\tconst char *signing_key);\n \n+/*\n+ * Similar to `sign_buffer()`, but uses the default configured signing key as\n+ * returned by `get_signing_key()` when the provided \"signing_key\" is NULL or\n+ * empty. Returns 0 on success, non-zero on failure.\n+ */\n+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t const char *signing_key);\n \n /*\n  * Returns corresponding string in lowercase for a given member of\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538631","messageId":"20260311173147.2336432-4-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260311173147.2336432-1-jltobler@gmail.com","subject":"[PATCH v4 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-11T17:31:47Z","receivedAt":"2026-03-11T17:31:58Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"With git-fast-import(1), handling of signed commits is controlled via\nthe `--signed-commits=<mode>` option. When an invalid signature is\nencountered, a user may want the option to sign the commit again as\nopposed to just stripping the signature. To facilitate this, introduce a\n\"sign-if-invalid\" mode for the `--signed-commits` option. Optionally, a\nkey ID may be explicitly provided in the form\n`sign-if-invalid[=<keyid>]` to specify which signing key should be used\nwhen signing invalid commit signatures.\n\nNote that to properly support interoperability mode when signing commit\nsignatures, the commit buffer must be created in both the repository and\ncompatability object formats to generate the appropriate signatures\naccordingly. As currently implemented, the commit buffer for the\ncompatability object format is not reconstructed and thus signing\ncommits in interoperability mode is not yet supported. Support may be\nadded in the future.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 100 ++++++++++++++++-----\n gpg-interface.c                    |  23 +++--\n gpg-interface.h                    |   7 +-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 6 files changed, 199 insertions(+), 83 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 479c4081da..b3f42d4637 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -86,6 +86,10 @@ already trusted to run their own code.\n * `strip-if-invalid` will check signatures and, if they are invalid,\n   will strip them and display a warning. The validation is performed\n   in the same way as linkgit:git-verify-commit[1] does it.\n+* `sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\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 \n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 0c5d2386d8..13621b0d6a 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -64,7 +64,7 @@ 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))\n+\tif (parse_sign_mode(arg, val, NULL))\n \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n \n \treturn 0;\n@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\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,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)\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}\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b8a7757cfd..d6281ff119 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -190,6 +190,7 @@ static const char *global_prefix;\n \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 \n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n@@ -2836,26 +2837,15 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n \tstrbuf_addbuf(new_data, msg);\n }\n \n-static void handle_strip_if_invalid(struct strbuf *new_data,\n-\t\t\t\t    struct signature_data *sig_sha1,\n-\t\t\t\t    struct signature_data *sig_sha256,\n-\t\t\t\t    struct strbuf *msg)\n+static void warn_invalid_signature(struct signature_check *check,\n+\t\t\t\t   const char *msg, enum sign_mode mode)\n {\n-\tstruct strbuf tmp_buf = STRBUF_INIT;\n-\tstruct signature_check signature_check = { 0 };\n-\tint ret;\n-\n-\t/* Check signature in a temporary commit buffer */\n-\tstrbuf_addbuf(&tmp_buf, new_data);\n-\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n-\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n-\n-\tif (ret) {\n-\t\tconst char *signer = signature_check.signer ?\n-\t\t\tsignature_check.signer : _(\"unknown\");\n-\t\tconst char *subject;\n-\t\tint subject_len = find_commit_subject(msg->buf, &subject);\n+\tconst char *signer = check->signer ? check->signer : _(\"unknown\");\n+\tconst char *subject;\n+\tint subject_len = find_commit_subject(msg, &subject);\n \n+\tswitch (mode) {\n+\tcase SIGN_STRIP_IF_INVALID:\n \t\tif (subject_len > 100)\n \t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n \t\t\t\t  \"  allegedly by %s\"), subject, signer);\n@@ -2865,6 +2855,66 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n \t\telse\n \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n \t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tcase SIGN_SIGN_IF_INVALID:\n+\t\tif (subject_len > 100)\n+\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n+\t\telse if (subject_len > 0)\n+\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n+\t\telse\n+\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unsupported signing mode\");\n+\t}\n+}\n+\n+static void handle_signature_if_invalid(struct strbuf *new_data,\n+\t\t\t\t\tstruct signature_data *sig_sha1,\n+\t\t\t\t\tstruct signature_data *sig_sha256,\n+\t\t\t\t\tstruct strbuf *msg,\n+\t\t\t\t\tenum sign_mode mode)\n+{\n+\tstruct strbuf tmp_buf = STRBUF_INIT;\n+\tstruct signature_check signature_check = { 0 };\n+\tint ret;\n+\n+\t/* Check signature in a temporary commit buffer */\n+\tstrbuf_addbuf(&tmp_buf, new_data);\n+\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n+\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n+\n+\tif (ret) {\n+\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n+\n+\t\tif (mode == SIGN_SIGN_IF_INVALID) {\n+\t\t\tstruct strbuf signature = STRBUF_INIT;\n+\t\t\tstruct strbuf payload = STRBUF_INIT;\n+\n+\t\t\t/*\n+\t\t\t * NEEDSWORK: To properly support interoperability mode\n+\t\t\t * when signing commit signatures, the commit buffer\n+\t\t\t * must be provided in both the repository and\n+\t\t\t * compatibility object formats. As currently\n+\t\t\t * implemented, only the repository object format is\n+\t\t\t * considered meaning compatibility signatures cannot be\n+\t\t\t * generated. Thus, attempting to sign commit signatures\n+\t\t\t * in interoperability mode is currently unsupported.\n+\t\t\t */\n+\t\t\tif (the_repository->compat_hash_algo)\n+\t\t\t\tdie(_(\"signing signatures in interoperability mode is unsupported\"));\n+\n+\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n+\t\t\tif (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))\n+\t\t\t\tdie(_(\"failed to sign commit object\"));\n+\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n+\n+\t\t\tstrbuf_release(&signature);\n+\t\t\tstrbuf_release(&payload);\n+\t\t}\n \n \t\tfinalize_commit_buffer(new_data, NULL, NULL, msg);\n \t} else {\n@@ -2927,6 +2977,7 @@ static void parse_new_commit(const char *arg)\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n \t\tcase SIGN_STRIP_IF_INVALID:\n+\t\tcase SIGN_SIGN_IF_INVALID:\n \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n@@ -3011,9 +3062,11 @@ static void parse_new_commit(const char *arg)\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n \n-\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n+\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n+\t     signed_commit_mode == SIGN_SIGN_IF_INVALID) &&\n \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n-\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n+\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n+\t\t\t\t\t    &msg, signed_commit_mode);\n \telse\n \t\tfinalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);\n \n@@ -3060,6 +3113,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\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 \tdefault:\n \t\tBUG(\"invalid signed_tag_mode value %d from tag '%s'\",\n \t\t    signed_tag_mode, name);\n@@ -3649,10 +3705,10 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_commit_mode))\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))\n+\t\tif (parse_sign_mode(option, &signed_tag_mode, NULL))\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/gpg-interface.c b/gpg-interface.c\nindex a72fa35061..2dd428ee2c 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1155,21 +1155,28 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \treturn ret;\n }\n \n-int parse_sign_mode(const char *arg, enum sign_mode *mode)\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)\n {\n-\tif (!strcmp(arg, \"abort\"))\n+\tif (!strcmp(arg, \"abort\")) {\n \t\t*mode = SIGN_ABORT;\n-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n+\t} else if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\")) {\n \t\t*mode = SIGN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n+\t} else if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\")) {\n \t\t*mode = SIGN_WARN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-strip\"))\n+\t} else if (!strcmp(arg, \"warn-strip\")) {\n \t\t*mode = SIGN_WARN_STRIP;\n-\telse if (!strcmp(arg, \"strip\"))\n+\t} else if (!strcmp(arg, \"strip\")) {\n \t\t*mode = SIGN_STRIP;\n-\telse if (!strcmp(arg, \"strip-if-invalid\"))\n+\t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n \t\t*mode = SIGN_STRIP_IF_INVALID;\n-\telse\n+\t} else if (!strcmp(arg, \"sign-if-invalid\")) {\n+\t\t*mode = SIGN_SIGN_IF_INVALID;\n+\t} else if (skip_prefix(arg, \"sign-if-invalid=\", &arg)) {\n+\t\t*mode = SIGN_SIGN_IF_INVALID;\n+\t\tif (keyid)\n+\t\t\t*keyid = arg;\n+\t} else {\n \t\treturn -1;\n+\t}\n \treturn 0;\n }\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex a32741aeda..25f6209fcf 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -119,12 +119,15 @@ enum sign_mode {\n \tSIGN_WARN_STRIP,\n \tSIGN_STRIP,\n \tSIGN_STRIP_IF_INVALID,\n+\tSIGN_SIGN_IF_INVALID,\n };\n \n /*\n  * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n- * otherwise.\n+ * otherwise. If the parsed mode is SIGN_SIGN_IF_INVALID and GPG key provided in\n+ * the arguments in the form `sign-if-invalid=<keyid>`, the key-ID is parsed\n+ * into `char **keyid`.\n  */\n-int parse_sign_mode(const char *arg, enum sign_mode *mode);\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);\n \n #endif\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nindex 022dae02e4..38b3e3b537 100755\n--- a/t/t9305-fast-import-signatures.sh\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -103,26 +103,85 @@ test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war\n \ttest_line_count = 2 out\n '\n \n-test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '\n-\tgit fast-export main >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n+for mode in strip-if-invalid sign-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+\t\tgit -C new fast-import --quiet --signed-commits=$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-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\t\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"handle signature invalidated by message change with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\n+\t\t# Change the commit message, which invalidates the signature.\n+\t\t# The commit message length should not change though, otherwise the\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\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+\t\ttest $OPENPGP_SIGNING != $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\n+\t\tif test \"$mode\" = strip-if-invalid\n+\t\tthen\n+\t\t\ttest_grep \"stripping invalid signature\" log &&\n+\t\t\ttest_grep ! -E \"^gpgsig\" actual\n+\t\telse\n+\t\t\ttest_grep \"signing commit with invalid signature\" log &&\n+\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\t\tgit -C new verify-commit \"$IMPORTED\"\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success GPGSM \"keep valid X.509 signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\t\ttest $X509_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSSH \"keep valid SSH signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\t\ttest $SSH_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+done\n+\n+test_expect_success GPGSSH \"sign invalid commit with explicit keyid\" '\n \trm -rf new &&\n \tgit init new &&\n \n@@ -133,41 +192,22 @@ test_expect_success GPG 'strip signature invalidated by message change with --si\n \t# corresponding `data <length>` command would have to be changed too.\n \tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n \n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n+\t# Configure the target repository with an invalid default signing key.\n+\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n+\ttest_config -C new gpg.format ssh &&\n+\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git -C new fast-import --quiet \\\n+\t\t--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&\n+\n+\t# Import using explicitly provided signing key.\n+\tgit -C new fast-import --quiet \\\n+\t\t--signed-commits=sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n \n \tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n \ttest $OPENPGP_SIGNING != $IMPORTED &&\n \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep ! -E \"^gpgsig\" actual &&\n-\ttest_grep \"stripping invalid signature\" log\n-'\n-\n-test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n-\ttest $X509_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n-\n-\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n-\ttest $SSH_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n+\tgit -C new verify-commit \"$IMPORTED\"\n '\n \n test_done\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538738","messageId":"abKT_50GVgBcj7op@pks.im","threadId":"65062","inReplyTo":"20260311173147.2336432-3-jltobler@gmail.com","subject":"Re: [PATCH v4 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-12T10:22:55Z","receivedAt":"2026-03-12T10:23:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 11, 2026 at 12:31:46PM -0500, Justin Tobler wrote:\n> diff --git a/gpg-interface.h b/gpg-interface.h\n> index 789d1ffac4..a32741aeda 100644\n> --- a/gpg-interface.h\n> +++ b/gpg-interface.h\n> @@ -83,6 +83,13 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n>  int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n>  \t\tconst char *signing_key);\n>  \n> +/*\n> + * Similar to `sign_buffer()`, but uses the default configured signing key as\n> + * returned by `get_signing_key()` when the provided \"signing_key\" is NULL or\n> + * empty. Returns 0 on success, non-zero on failure.\n> + */\n> +int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t const char *signing_key);\n\nI think this interface is a bit confusing, as you wouldn't really be\nable to tell what the difference between `sign_buffer()` and\n`sign_buffer_with_key()` is without having a deeper look. Naively, I\nwould expect the latter function to be the one that actually mandates\nthat the user provides a key, but it's the other way round.\n\nWould it be preferable to instead extend `sign_buffer()` to take a flags\nparameter and then introduce `SIGN_BUFFER_USE_DEFAULT_KEY` to make it\nfall back to the configured signing key? If so, we could drop\n`sign_commit_to_strbuf()` completely.\n\nPatrick\n"},{"id":"538739","messageId":"abKUBRRgRmbJ1hRA@pks.im","threadId":"65062","inReplyTo":"20260311173147.2336432-4-jltobler@gmail.com","subject":"Re: [PATCH v4 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-12T10:23:01Z","receivedAt":"2026-03-12T10:23:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Mar 11, 2026 at 12:31:47PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index b8a7757cfd..d6281ff119 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -2865,6 +2855,66 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n>  \t\telse\n>  \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n>  \t\t\t\t  \"  allegedly by %s\"), signer);\n> +\t\tbreak;\n> +\tcase SIGN_SIGN_IF_INVALID:\n> +\t\tif (subject_len > 100)\n> +\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n> +\t\telse if (subject_len > 0)\n> +\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n> +\t\telse\n> +\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), signer);\n> +\t\tbreak;\n> +\tdefault:\n> +\t\tBUG(\"unsupported signing mode\");\n> +\t}\n> +}\n\nI'm still not convinced that it makes sense to warn about this case.\nAfter all the user has asked us to re-sign such commits, so they\nprobably expect such cases. These warnings would thus result in a ton of\nnoise in a repository where most commits are signed, drowning out the\npotentially-useful warnings.\n\nAnyway, I won't insist on a change here.\n\n> +static void handle_signature_if_invalid(struct strbuf *new_data,\n> +\t\t\t\t\tstruct signature_data *sig_sha1,\n> +\t\t\t\t\tstruct signature_data *sig_sha256,\n> +\t\t\t\t\tstruct strbuf *msg,\n> +\t\t\t\t\tenum sign_mode mode)\n> +{\n> +\tstruct strbuf tmp_buf = STRBUF_INIT;\n> +\tstruct signature_check signature_check = { 0 };\n> +\tint ret;\n> +\n> +\t/* Check signature in a temporary commit buffer */\n> +\tstrbuf_addbuf(&tmp_buf, new_data);\n> +\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n> +\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n> +\n> +\tif (ret) {\n> +\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n> +\n> +\t\tif (mode == SIGN_SIGN_IF_INVALID) {\n> +\t\t\tstruct strbuf signature = STRBUF_INIT;\n> +\t\t\tstruct strbuf payload = STRBUF_INIT;\n> +\n> +\t\t\t/*\n> +\t\t\t * NEEDSWORK: To properly support interoperability mode\n> +\t\t\t * when signing commit signatures, the commit buffer\n> +\t\t\t * must be provided in both the repository and\n> +\t\t\t * compatibility object formats. As currently\n> +\t\t\t * implemented, only the repository object format is\n> +\t\t\t * considered meaning compatibility signatures cannot be\n> +\t\t\t * generated. Thus, attempting to sign commit signatures\n> +\t\t\t * in interoperability mode is currently unsupported.\n> +\t\t\t */\n> +\t\t\tif (the_repository->compat_hash_algo)\n> +\t\t\t\tdie(_(\"signing signatures in interoperability mode is unsupported\"));\n\n\"signing signatures\"? You probably meant \"signing commits\"?\n\nPatrick\n"},{"id":"538748","messageId":"abLF-08jpqT8jYpp@denethor","threadId":"65062","inReplyTo":"abKT_50GVgBcj7op@pks.im","subject":"Re: [PATCH v4 2/3] gpg-interface: introduce sign_buffer_with_key()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T13:58:06Z","receivedAt":"2026-03-12T13:58:08Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/12 11:22AM, Patrick Steinhardt wrote:\n> On Wed, Mar 11, 2026 at 12:31:46PM -0500, Justin Tobler wrote:\n> > diff --git a/gpg-interface.h b/gpg-interface.h\n> > index 789d1ffac4..a32741aeda 100644\n> > --- a/gpg-interface.h\n> > +++ b/gpg-interface.h\n> > @@ -83,6 +83,13 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n> >  int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n> >  \t\tconst char *signing_key);\n> >  \n> > +/*\n> > + * Similar to `sign_buffer()`, but uses the default configured signing key as\n> > + * returned by `get_signing_key()` when the provided \"signing_key\" is NULL or\n> > + * empty. Returns 0 on success, non-zero on failure.\n> > + */\n> > +int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n> > +\t\t\t const char *signing_key);\n> \n> I think this interface is a bit confusing, as you wouldn't really be\n> able to tell what the difference between `sign_buffer()` and\n> `sign_buffer_with_key()` is without having a deeper look. Naively, I\n> would expect the latter function to be the one that actually mandates\n> that the user provides a key, but it's the other way round.\n> \n> Would it be preferable to instead extend `sign_buffer()` to take a flags\n> parameter and then introduce `SIGN_BUFFER_USE_DEFAULT_KEY` to make it\n> fall back to the configured signing key? If so, we could drop\n> `sign_commit_to_strbuf()` completely.\n\nThat's fair, and this suggestion sounds completely sensible to me. There\nare only a handful to `sign_buffer()` callers, so it should create too\nmuch churn either. Will send another version adapted accordingly.\n\n-Justin\n"},{"id":"538749","messageId":"abLGgq-PXzdWs6kD@denethor","threadId":"65062","inReplyTo":"abKUBRRgRmbJ1hRA@pks.im","subject":"Re: [PATCH v4 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T14:08:46Z","receivedAt":"2026-03-12T14:08:51Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/12 11:23AM, Patrick Steinhardt wrote:\n> On Wed, Mar 11, 2026 at 12:31:47PM -0500, Justin Tobler wrote:\n> > diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> > index b8a7757cfd..d6281ff119 100644\n> > --- a/builtin/fast-import.c\n> > +++ b/builtin/fast-import.c\n> > @@ -2865,6 +2855,66 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n> >  \t\telse\n> >  \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n> >  \t\t\t\t  \"  allegedly by %s\"), signer);\n> > +\t\tbreak;\n> > +\tcase SIGN_SIGN_IF_INVALID:\n> > +\t\tif (subject_len > 100)\n> > +\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n> > +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n> > +\t\telse if (subject_len > 0)\n> > +\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n> > +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n> > +\t\telse\n> > +\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n> > +\t\t\t\t  \"  allegedly by %s\"), signer);\n> > +\t\tbreak;\n> > +\tdefault:\n> > +\t\tBUG(\"unsupported signing mode\");\n> > +\t}\n> > +}\n> \n> I'm still not convinced that it makes sense to warn about this case.\n> After all the user has asked us to re-sign such commits, so they\n> probably expect such cases. These warnings would thus result in a ton of\n> noise in a repository where most commits are signed, drowning out the\n> potentially-useful warnings.\n> \n> Anyway, I won't insist on a change here.\n\nI'm not really against removing these warning as I also agree it creates\na bunch of noise. If we get rid of them for \"sign-if-invalid\" though,\nshouldn't we also get rid of them for \"strip-if-invalid\"? If the user\nasks to strip commits, I figure they would expect such cases as well. If\nwe think removing the warning altogether is sensible, I can add another\nprepatory commit that simply removes the warning for the\n\"strip-if-invalid\" case.\n\n> > +static void handle_signature_if_invalid(struct strbuf *new_data,\n> > +\t\t\t\t\tstruct signature_data *sig_sha1,\n> > +\t\t\t\t\tstruct signature_data *sig_sha256,\n> > +\t\t\t\t\tstruct strbuf *msg,\n> > +\t\t\t\t\tenum sign_mode mode)\n> > +{\n> > +\tstruct strbuf tmp_buf = STRBUF_INIT;\n> > +\tstruct signature_check signature_check = { 0 };\n> > +\tint ret;\n> > +\n> > +\t/* Check signature in a temporary commit buffer */\n> > +\tstrbuf_addbuf(&tmp_buf, new_data);\n> > +\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n> > +\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n> > +\n> > +\tif (ret) {\n> > +\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n> > +\n> > +\t\tif (mode == SIGN_SIGN_IF_INVALID) {\n> > +\t\t\tstruct strbuf signature = STRBUF_INIT;\n> > +\t\t\tstruct strbuf payload = STRBUF_INIT;\n> > +\n> > +\t\t\t/*\n> > +\t\t\t * NEEDSWORK: To properly support interoperability mode\n> > +\t\t\t * when signing commit signatures, the commit buffer\n> > +\t\t\t * must be provided in both the repository and\n> > +\t\t\t * compatibility object formats. As currently\n> > +\t\t\t * implemented, only the repository object format is\n> > +\t\t\t * considered meaning compatibility signatures cannot be\n> > +\t\t\t * generated. Thus, attempting to sign commit signatures\n> > +\t\t\t * in interoperability mode is currently unsupported.\n> > +\t\t\t */\n> > +\t\t\tif (the_repository->compat_hash_algo)\n> > +\t\t\t\tdie(_(\"signing signatures in interoperability mode is unsupported\"));\n> \n> \"signing signatures\"? You probably meant \"signing commits\"?\n\nAh yes! Will fix in the next version. Thanks for reading closely :)\n\n-Justin\n"},{"id":"538751","messageId":"abLMCxWWNiCnqmp_@pks.im","threadId":"65062","inReplyTo":"abLGgq-PXzdWs6kD@denethor","subject":"Re: [PATCH v4 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-12T14:22:03Z","receivedAt":"2026-03-12T14:22:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Mar 12, 2026 at 09:08:46AM -0500, Justin Tobler wrote:\n> On 26/03/12 11:23AM, Patrick Steinhardt wrote:\n> > On Wed, Mar 11, 2026 at 12:31:47PM -0500, Justin Tobler wrote:\n> > > diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> > > index b8a7757cfd..d6281ff119 100644\n> > > --- a/builtin/fast-import.c\n> > > +++ b/builtin/fast-import.c\n> > > @@ -2865,6 +2855,66 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n> > >  \t\telse\n> > >  \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n> > >  \t\t\t\t  \"  allegedly by %s\"), signer);\n> > > +\t\tbreak;\n> > > +\tcase SIGN_SIGN_IF_INVALID:\n> > > +\t\tif (subject_len > 100)\n> > > +\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n> > > +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n> > > +\t\telse if (subject_len > 0)\n> > > +\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n> > > +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n> > > +\t\telse\n> > > +\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n> > > +\t\t\t\t  \"  allegedly by %s\"), signer);\n> > > +\t\tbreak;\n> > > +\tdefault:\n> > > +\t\tBUG(\"unsupported signing mode\");\n> > > +\t}\n> > > +}\n> > \n> > I'm still not convinced that it makes sense to warn about this case.\n> > After all the user has asked us to re-sign such commits, so they\n> > probably expect such cases. These warnings would thus result in a ton of\n> > noise in a repository where most commits are signed, drowning out the\n> > potentially-useful warnings.\n> > \n> > Anyway, I won't insist on a change here.\n> \n> I'm not really against removing these warning as I also agree it creates\n> a bunch of noise. If we get rid of them for \"sign-if-invalid\" though,\n> shouldn't we also get rid of them for \"strip-if-invalid\"? If the user\n> asks to strip commits, I figure they would expect such cases as well. If\n> we think removing the warning altogether is sensible, I can add another\n> prepatory commit that simply removes the warning for the\n> \"strip-if-invalid\" case.\n\nYeah, it kind of falls into the same space, agreed. As said, I won't\ninsist on changing this. Maybe the right way to approach this is to keep\nit as-is for now and create a follow-up patch where you propose to strip\nit from both sites?\n\nPatrick\n"},{"id":"538776","messageId":"abL1Nkb_9aNUcUnY@denethor","threadId":"65062","inReplyTo":"abLMCxWWNiCnqmp_@pks.im","subject":"Re: [PATCH v4 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T17:21:16Z","receivedAt":"2026-03-12T17:21:21Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/12 03:22PM, Patrick Steinhardt wrote:\n> On Thu, Mar 12, 2026 at 09:08:46AM -0500, Justin Tobler wrote:\n> > On 26/03/12 11:23AM, Patrick Steinhardt wrote:\n> > > I'm still not convinced that it makes sense to warn about this case.\n> > > After all the user has asked us to re-sign such commits, so they\n> > > probably expect such cases. These warnings would thus result in a ton of\n> > > noise in a repository where most commits are signed, drowning out the\n> > > potentially-useful warnings.\n> > > \n> > > Anyway, I won't insist on a change here.\n> > \n> > I'm not really against removing these warning as I also agree it creates\n> > a bunch of noise. If we get rid of them for \"sign-if-invalid\" though,\n> > shouldn't we also get rid of them for \"strip-if-invalid\"? If the user\n> > asks to strip commits, I figure they would expect such cases as well. If\n> > we think removing the warning altogether is sensible, I can add another\n> > prepatory commit that simply removes the warning for the\n> > \"strip-if-invalid\" case.\n> \n> Yeah, it kind of falls into the same space, agreed. As said, I won't\n> insist on changing this. Maybe the right way to approach this is to keep\n> it as-is for now and create a follow-up patch where you propose to strip\n> it from both sites?\n\nThat's fair. I'll leave it as-is for now and submit a separate follow up\npatch after this gets merged that proposes removing the warnings\naltogether. We can see what folks think about it there.\n\nThanks,\n-Justin\n"},{"id":"538795","messageId":"20260312192228.481134-1-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260311173147.2336432-1-jltobler@gmail.com","subject":"[PATCH v5 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T19:22:25Z","receivedAt":"2026-03-12T19:22:34Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nWith c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17), it became possible to remove\ninvalid signatures from commits via git-fast-import(1) while maintaining\nvalid commit signatures. Building upon this functionality, a user may\nwant to re-sign these invalid commit signatures. This series introduces\nthe `sign-if-invalid` mode to do so accordingly.\n\nThe newly added mode in this series currently ignores\n`extensions.compatObjectFormat` when generating the new signatures. From\nmy understanding, to generate the compatibility structure would also\nrequire us to reconstruct the compatibility object for the object being\nsigned. I think this would be possible to do, but would require getting\nthe mapped OIDs for the commit parents and tree. I'm not completely sure\nof a good way to go about this yet though. I'm also not completely\ncertain if this is something that should be addressed as part of this\nseries, or could be done later down the road. So for now I've opted to\ndelay its implementation. I'm open going down the other route if that is\npreferred though.\n\nThe first commit is a simple cleanup for something I noticed while\nreading though commit signing code. The second commit actually\nintroduces the new `--signed-commits` mode.\n\nChanges since V4:\n- Instead of introducing a separate `sign_buffer_with_key()` helper,\n  extend `sign_buffer()` to support a SIGN_BUFFER_USE_DEFAULT_KEY flag.\n- Fixed message in die().\n\nChanges since V3:\n- Rename the `re-sign-if-invalid` mode to `sign-if-invalid`. The\n  \"if-invalid\" already implies the signatures was previously signed\n  making \"re-sign\" redundant.\n\nChanges since V2:\n- Adapted commit message in second patch to improve clarity.\n- Fixed typos.\n- Renamed SIGN_RESIGN_IF_INVALID to SIGN_RE_SIGN_IF_INVALID.\n- Created separate helper function to handle printing invalid signature\n  warnings.\n\nChanges since V1:\n- Improved commit messages and comments to better explain why\n  interoperability mode is not currently supported.\n- Clarified documentation for re-sign-if-invalid mode.\n- Renamed `handle_invalid_signature()` to `handle_signature_if_invalid()`.\n- Added warning messages specific to commit resigning.\n- Fixed some small typos.\n- Added support for explicitly specifying the signing key ID via\n  `--signed-commits=re-sign-if-invalid[=<keyid>]` similar to how it can\n  specified in git-commit(1).\n- We now die() as unsupported when attempting to re-sign an invalid\n  commit signature in interoperability mode.\n- We now die() when failing to re-sign a commit.\n\nThanks,\n-Justin\n\nJustin Tobler (3):\n  commit: remove unused forward declaration\n  gpg-interface: allow sign_buffer() to use default signing key\n  fast-import: add mode to sign commits with invalid signatures\n\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 101 ++++++++++++++++-----\n builtin/tag.c                      |   4 +-\n commit.c                           |  19 ++--\n commit.h                           |   2 -\n gpg-interface.c                    |  36 +++++---\n gpg-interface.h                    |  19 +++-\n send-pack.c                        |   2 +-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 10 files changed, 229 insertions(+), 106 deletions(-)\n\nRange-diff against v4:\n1:  0d00b72ee0 = 1:  0d00b72ee0 commit: remove unused forward declaration\n2:  87f590f1f8 ! 2:  7a0deed77b gpg-interface: introduce sign_buffer_with_key()\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    gpg-interface: introduce sign_buffer_with_key()\n    +    gpg-interface: allow sign_buffer() to use default signing key\n     \n         The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\n         logic to get the default configured signing key when a key is not\n    @@ Commit message\n         reused by git-fast-import(1) when signing commits with invalid\n         signatures.\n     \n    -    Move the `sign_commit_to_strbuf()` helper from \"commit.c\" to\n    -    \"gpg-interface.c\" and rename it to `sign_buffer_with_key()`. Also export\n    -    this function so it can be used by \"commit.c\" and\n    -    \"builtin/fast-import.c\" in the subsequent commit.\n    +    Remove the `sign_commit_to_strbuf()` helper from \"commit.c\" and extend\n    +    `sign_buffer()` in \"gpg-interface.c\" to support using the default key as\n    +    a fallback when the `SIGN_BUFFER_USE_DEFAULT_KEY` flag is provided. Call\n    +    sites are updated accordingly.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n    + ## builtin/tag.c ##\n    +@@ builtin/tag.c: static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,\n    + \tchar *keyid = get_signing_key();\n    + \tint ret = -1;\n    + \n    +-\tif (sign_buffer(buffer, &sig, keyid))\n    ++\tif (sign_buffer(buffer, &sig, keyid, 0))\n    + \t\tgoto out;\n    + \n    + \tif (compat) {\n    +@@ builtin/tag.c: static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,\n    + \t\tif (convert_object_file(the_repository ,&compat_buf, algo, compat,\n    + \t\t\t\t\tbuffer->buf, buffer->len, OBJ_TAG, 1))\n    + \t\t\tgoto out;\n    +-\t\tif (sign_buffer(&compat_buf, &compat_sig, keyid))\n    ++\t\tif (sign_buffer(&compat_buf, &compat_sig, keyid, 0))\n    + \t\t\tgoto out;\n    + \t\tadd_header_signature(&compat_buf, &sig, algo);\n    + \t\tstrbuf_addbuf(&compat_buf, &compat_sig);\n    +\n      ## commit.c ##\n     @@ commit.c: int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi\n      \treturn 0;\n    @@ commit.c: int commit_tree_extended(const char *msg, size_t msg_len,\n      \n      \twrite_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);\n     -\tif (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {\n    -+\tif (sign_commit && sign_buffer_with_key(&buffer, &sig, sign_commit)) {\n    ++\tif (sign_commit && sign_buffer(&buffer, &sig, sign_commit,\n    ++\t\t\t\t       SIGN_BUFFER_USE_DEFAULT_KEY)) {\n      \t\tresult = -1;\n      \t\tgoto out;\n      \t}\n    @@ commit.c: int commit_tree_extended(const char *msg, size_t msg_len,\n      \t\tfree(mapped_parents);\n      \n     -\t\tif (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {\n    -+\t\tif (sign_commit && sign_buffer_with_key(&compat_buffer, &compat_sig, sign_commit)) {\n    ++\t\tif (sign_commit && sign_buffer(&compat_buffer, &compat_sig,\n    ++\t\t\t\t\t       sign_commit,\n    ++\t\t\t\t\t       SIGN_BUFFER_USE_DEFAULT_KEY)) {\n      \t\t\tresult = -1;\n      \t\t\tgoto out;\n      \t\t}\n     \n      ## gpg-interface.c ##\n    -@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n    - \treturn use_format->sign_buffer(buffer, signature, signing_key);\n    +@@ gpg-interface.c: const char *gpg_trust_level_to_str(enum signature_trust_level level)\n    + \treturn sigcheck_gpg_trust_level[level].display_key;\n      }\n      \n    -+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n    -+\t\t\t const char *signing_key)\n    -+{\n    +-int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n    ++int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n    ++\t\tconst char *signing_key, enum sign_buffer_flags flags)\n    + {\n     +\tchar *keyid_to_free = NULL;\n     +\tint ret = 0;\n    -+\tif (!signing_key || !*signing_key)\n    ++\n    + \tgpg_interface_lazy_init();\n    + \n    +-\treturn use_format->sign_buffer(buffer, signature, signing_key);\n    ++\tif (flags & SIGN_BUFFER_USE_DEFAULT_KEY && (!signing_key || !*signing_key))\n     +\t\tsigning_key = keyid_to_free = get_signing_key();\n    -+\tif (sign_buffer(buffer, signature, signing_key))\n    -+\t\tret = -1;\n    ++\n    ++\tret = use_format->sign_buffer(buffer, signature, signing_key);\n     +\tfree(keyid_to_free);\n     +\treturn ret;\n    -+}\n    -+\n    + }\n    + \n      /*\n    -  * Strip CR from the line endings, in case we are on Windows.\n    -  * NEEDSWORK: make it trim only CRs before LFs and rename\n     \n      ## gpg-interface.h ##\n    +@@ gpg-interface.h: int parse_signature(const char *buf, size_t size, struct strbuf *payload, struct\n    +  */\n    + size_t parse_signed_buffer(const char *buf, size_t size);\n    + \n    ++/* Flags for sign_buffer(). */\n    ++enum sign_buffer_flags {\n    ++\t/*\n    ++\t * Use the default configured signing key as returned by `get_signing_key()`\n    ++\t * when the provided \"signing_key\" is NULL or empty.\n    ++\t */\n    ++\tSIGN_BUFFER_USE_DEFAULT_KEY = (1 << 0),\n    ++};\n    ++\n    + /*\n    +  * Create a detached signature for the contents of \"buffer\" and append\n    +  * it after \"signature\"; \"buffer\" and \"signature\" can be the same\n     @@ gpg-interface.h: size_t parse_signed_buffer(const char *buf, size_t size);\n    +  * at the end.  Returns 0 on success, non-zero on failure.\n    +  */\n      int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n    - \t\tconst char *signing_key);\n    - \n    -+/*\n    -+ * Similar to `sign_buffer()`, but uses the default configured signing key as\n    -+ * returned by `get_signing_key()` when the provided \"signing_key\" is NULL or\n    -+ * empty. Returns 0 on success, non-zero on failure.\n    -+ */\n    -+int sign_buffer_with_key(struct strbuf *buffer, struct strbuf *signature,\n    -+\t\t\t const char *signing_key);\n    +-\t\tconst char *signing_key);\n    +-\n    ++\t\tconst char *signing_key, enum sign_buffer_flags flags);\n      \n      /*\n       * Returns corresponding string in lowercase for a given member of\n    +\n    + ## send-pack.c ##\n    +@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,\n    + \tif (!update_seen)\n    + \t\tgoto free_return;\n    + \n    +-\tif (sign_buffer(&cert, &cert, signing_key))\n    ++\tif (sign_buffer(&cert, &cert, signing_key, 0))\n    + \t\tdie(_(\"failed to sign the push certificate\"));\n    + \n    + \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n3:  8b01ad1570 ! 3:  e659971e84 fast-import: add mode to sign commits with invalid signatures\n    @@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_da\n     +\t\t\t * in interoperability mode is currently unsupported.\n     +\t\t\t */\n     +\t\t\tif (the_repository->compat_hash_algo)\n    -+\t\t\t\tdie(_(\"signing signatures in interoperability mode is unsupported\"));\n    ++\t\t\t\tdie(_(\"signing commits in interoperability mode is unsupported\"));\n     +\n     +\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n    -+\t\t\tif (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))\n    ++\t\t\tif (sign_buffer(&payload, &signature, signed_commit_keyid,\n    ++\t\t\t\t\tSIGN_BUFFER_USE_DEFAULT_KEY))\n     +\t\t\t\tdie(_(\"failed to sign commit object\"));\n     +\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n     +\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538796","messageId":"20260312192228.481134-2-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260312192228.481134-1-jltobler@gmail.com","subject":"[PATCH v5 1/3] commit: remove unused forward declaration","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T19:22:26Z","receivedAt":"2026-03-12T19:22:35Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n`sign_with_header()` was removed, but its forward declaration in\n\"commit.h\" was left. Remove the unused declaration.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.h | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/commit.h b/commit.h\nindex 1635de418b..f0c38cb444 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -390,8 +390,6 @@ LAST_ARG_MUST_BE_NULL\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook, const char *name, ...);\n \n-/* Sign a commit or tag buffer, storing the result in a header. */\n-int sign_with_header(struct strbuf *buf, const char *keyid);\n /* Parse the signature out of a header. */\n int parse_buffer_signed_by_header(const char *buffer,\n \t\t\t\t  unsigned long size,\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538797","messageId":"20260312192228.481134-3-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260312192228.481134-1-jltobler@gmail.com","subject":"[PATCH v5 2/3] gpg-interface: allow sign_buffer() to use default signing key","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T19:22:27Z","receivedAt":"2026-03-12T19:22:36Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\nlogic to get the default configured signing key when a key is not\nprovided and handles generating the commit signature accordingly. This\nsigning operation is not really specific to commits as any arbitrary\nbuffer can be signed. Also, in a subsequent commit, this same logic is\nreused by git-fast-import(1) when signing commits with invalid\nsignatures.\n\nRemove the `sign_commit_to_strbuf()` helper from \"commit.c\" and extend\n`sign_buffer()` in \"gpg-interface.c\" to support using the default key as\na fallback when the `SIGN_BUFFER_USE_DEFAULT_KEY` flag is provided. Call\nsites are updated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/tag.c   |  4 ++--\n commit.c        | 19 +++++--------------\n gpg-interface.c | 13 +++++++++++--\n gpg-interface.h | 12 ++++++++++--\n send-pack.c     |  2 +-\n 5 files changed, 29 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex aeb04c487f..540d783c67 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -167,7 +167,7 @@ static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,\n \tchar *keyid = get_signing_key();\n \tint ret = -1;\n \n-\tif (sign_buffer(buffer, &sig, keyid))\n+\tif (sign_buffer(buffer, &sig, keyid, 0))\n \t\tgoto out;\n \n \tif (compat) {\n@@ -176,7 +176,7 @@ static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,\n \t\tif (convert_object_file(the_repository ,&compat_buf, algo, compat,\n \t\t\t\t\tbuffer->buf, buffer->len, OBJ_TAG, 1))\n \t\t\tgoto out;\n-\t\tif (sign_buffer(&compat_buf, &compat_sig, keyid))\n+\t\tif (sign_buffer(&compat_buf, &compat_sig, keyid, 0))\n \t\t\tgoto out;\n \t\tadd_header_signature(&compat_buf, &sig, algo);\n \t\tstrbuf_addbuf(&compat_buf, &compat_sig);\ndiff --git a/commit.c b/commit.c\nindex d16ae73345..1b9b2d4499 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1148,18 +1148,6 @@ int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi\n \treturn 0;\n }\n \n-static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)\n-{\n-\tchar *keyid_to_free = NULL;\n-\tint ret = 0;\n-\tif (!keyid || !*keyid)\n-\t\tkeyid = keyid_to_free = get_signing_key();\n-\tif (sign_buffer(buf, sig, keyid))\n-\t\tret = -1;\n-\tfree(keyid_to_free);\n-\treturn ret;\n-}\n-\n int parse_signed_commit(const struct commit *commit,\n \t\t\tstruct strbuf *payload, struct strbuf *signature,\n \t\t\tconst struct git_hash_algo *algop)\n@@ -1737,7 +1725,8 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\toidcpy(&parent_buf[i++], &p->item->object.oid);\n \n \twrite_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);\n-\tif (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {\n+\tif (sign_commit && sign_buffer(&buffer, &sig, sign_commit,\n+\t\t\t\t       SIGN_BUFFER_USE_DEFAULT_KEY)) {\n \t\tresult = -1;\n \t\tgoto out;\n \t}\n@@ -1769,7 +1758,9 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\tfree_commit_extra_headers(compat_extra);\n \t\tfree(mapped_parents);\n \n-\t\tif (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {\n+\t\tif (sign_commit && sign_buffer(&compat_buffer, &compat_sig,\n+\t\t\t\t\t       sign_commit,\n+\t\t\t\t\t       SIGN_BUFFER_USE_DEFAULT_KEY)) {\n \t\t\tresult = -1;\n \t\t\tgoto out;\n \t\t}\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 87fb6605fb..ce935908cc 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -973,11 +973,20 @@ const char *gpg_trust_level_to_str(enum signature_trust_level level)\n \treturn sigcheck_gpg_trust_level[level].display_key;\n }\n \n-int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n+int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n+\t\tconst char *signing_key, enum sign_buffer_flags flags)\n {\n+\tchar *keyid_to_free = NULL;\n+\tint ret = 0;\n+\n \tgpg_interface_lazy_init();\n \n-\treturn use_format->sign_buffer(buffer, signature, signing_key);\n+\tif (flags & SIGN_BUFFER_USE_DEFAULT_KEY && (!signing_key || !*signing_key))\n+\t\tsigning_key = keyid_to_free = get_signing_key();\n+\n+\tret = use_format->sign_buffer(buffer, signature, signing_key);\n+\tfree(keyid_to_free);\n+\treturn ret;\n }\n \n /*\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 789d1ffac4..37f3ac42db 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -74,6 +74,15 @@ int parse_signature(const char *buf, size_t size, struct strbuf *payload, struct\n  */\n size_t parse_signed_buffer(const char *buf, size_t size);\n \n+/* Flags for sign_buffer(). */\n+enum sign_buffer_flags {\n+\t/*\n+\t * Use the default configured signing key as returned by `get_signing_key()`\n+\t * when the provided \"signing_key\" is NULL or empty.\n+\t */\n+\tSIGN_BUFFER_USE_DEFAULT_KEY = (1 << 0),\n+};\n+\n /*\n  * Create a detached signature for the contents of \"buffer\" and append\n  * it after \"signature\"; \"buffer\" and \"signature\" can be the same\n@@ -81,8 +90,7 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n  * at the end.  Returns 0 on success, non-zero on failure.\n  */\n int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n-\t\tconst char *signing_key);\n-\n+\t\tconst char *signing_key, enum sign_buffer_flags flags);\n \n /*\n  * Returns corresponding string in lowercase for a given member of\ndiff --git a/send-pack.c b/send-pack.c\nindex 67d6987b1c..07ecfae4de 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -391,7 +391,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, signing_key, 0))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538798","messageId":"20260312192228.481134-4-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260312192228.481134-1-jltobler@gmail.com","subject":"[PATCH v5 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T19:22:28Z","receivedAt":"2026-03-12T19:22:36Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"With git-fast-import(1), handling of signed commits is controlled via\nthe `--signed-commits=<mode>` option. When an invalid signature is\nencountered, a user may want the option to sign the commit again as\nopposed to just stripping the signature. To facilitate this, introduce a\n\"sign-if-invalid\" mode for the `--signed-commits` option. Optionally, a\nkey ID may be explicitly provided in the form\n`sign-if-invalid[=<keyid>]` to specify which signing key should be used\nwhen signing invalid commit signatures.\n\nNote that to properly support interoperability mode when signing commit\nsignatures, the commit buffer must be created in both the repository and\ncompatability object formats to generate the appropriate signatures\naccordingly. As currently implemented, the commit buffer for the\ncompatability object format is not reconstructed and thus signing\ncommits in interoperability mode is not yet supported. Support may be\nadded in the future.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 101 ++++++++++++++++-----\n gpg-interface.c                    |  23 +++--\n gpg-interface.h                    |   7 +-\n t/t9305-fast-import-signatures.sh  | 140 ++++++++++++++++++-----------\n 6 files changed, 200 insertions(+), 83 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 479c4081da..b3f42d4637 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -86,6 +86,10 @@ already trusted to run their own code.\n * `strip-if-invalid` will check signatures and, if they are invalid,\n   will strip them and display a warning. The validation is performed\n   in the same way as linkgit:git-verify-commit[1] does it.\n+* `sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\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 \n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 0c5d2386d8..13621b0d6a 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -64,7 +64,7 @@ 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))\n+\tif (parse_sign_mode(arg, val, NULL))\n \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n \n \treturn 0;\n@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\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,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)\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}\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b8a7757cfd..50de88e2ea 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -190,6 +190,7 @@ static const char *global_prefix;\n \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 \n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n@@ -2836,26 +2837,15 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n \tstrbuf_addbuf(new_data, msg);\n }\n \n-static void handle_strip_if_invalid(struct strbuf *new_data,\n-\t\t\t\t    struct signature_data *sig_sha1,\n-\t\t\t\t    struct signature_data *sig_sha256,\n-\t\t\t\t    struct strbuf *msg)\n+static void warn_invalid_signature(struct signature_check *check,\n+\t\t\t\t   const char *msg, enum sign_mode mode)\n {\n-\tstruct strbuf tmp_buf = STRBUF_INIT;\n-\tstruct signature_check signature_check = { 0 };\n-\tint ret;\n-\n-\t/* Check signature in a temporary commit buffer */\n-\tstrbuf_addbuf(&tmp_buf, new_data);\n-\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n-\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n-\n-\tif (ret) {\n-\t\tconst char *signer = signature_check.signer ?\n-\t\t\tsignature_check.signer : _(\"unknown\");\n-\t\tconst char *subject;\n-\t\tint subject_len = find_commit_subject(msg->buf, &subject);\n+\tconst char *signer = check->signer ? check->signer : _(\"unknown\");\n+\tconst char *subject;\n+\tint subject_len = find_commit_subject(msg, &subject);\n \n+\tswitch (mode) {\n+\tcase SIGN_STRIP_IF_INVALID:\n \t\tif (subject_len > 100)\n \t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n \t\t\t\t  \"  allegedly by %s\"), subject, signer);\n@@ -2865,6 +2855,67 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n \t\telse\n \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n \t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tcase SIGN_SIGN_IF_INVALID:\n+\t\tif (subject_len > 100)\n+\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n+\t\telse if (subject_len > 0)\n+\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n+\t\telse\n+\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unsupported signing mode\");\n+\t}\n+}\n+\n+static void handle_signature_if_invalid(struct strbuf *new_data,\n+\t\t\t\t\tstruct signature_data *sig_sha1,\n+\t\t\t\t\tstruct signature_data *sig_sha256,\n+\t\t\t\t\tstruct strbuf *msg,\n+\t\t\t\t\tenum sign_mode mode)\n+{\n+\tstruct strbuf tmp_buf = STRBUF_INIT;\n+\tstruct signature_check signature_check = { 0 };\n+\tint ret;\n+\n+\t/* Check signature in a temporary commit buffer */\n+\tstrbuf_addbuf(&tmp_buf, new_data);\n+\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n+\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n+\n+\tif (ret) {\n+\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n+\n+\t\tif (mode == SIGN_SIGN_IF_INVALID) {\n+\t\t\tstruct strbuf signature = STRBUF_INIT;\n+\t\t\tstruct strbuf payload = STRBUF_INIT;\n+\n+\t\t\t/*\n+\t\t\t * NEEDSWORK: To properly support interoperability mode\n+\t\t\t * when signing commit signatures, the commit buffer\n+\t\t\t * must be provided in both the repository and\n+\t\t\t * compatibility object formats. As currently\n+\t\t\t * implemented, only the repository object format is\n+\t\t\t * considered meaning compatibility signatures cannot be\n+\t\t\t * generated. Thus, attempting to sign commit signatures\n+\t\t\t * in interoperability mode is currently unsupported.\n+\t\t\t */\n+\t\t\tif (the_repository->compat_hash_algo)\n+\t\t\t\tdie(_(\"signing commits in interoperability mode is unsupported\"));\n+\n+\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n+\t\t\tif (sign_buffer(&payload, &signature, signed_commit_keyid,\n+\t\t\t\t\tSIGN_BUFFER_USE_DEFAULT_KEY))\n+\t\t\t\tdie(_(\"failed to sign commit object\"));\n+\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n+\n+\t\t\tstrbuf_release(&signature);\n+\t\t\tstrbuf_release(&payload);\n+\t\t}\n \n \t\tfinalize_commit_buffer(new_data, NULL, NULL, msg);\n \t} else {\n@@ -2927,6 +2978,7 @@ static void parse_new_commit(const char *arg)\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n \t\tcase SIGN_STRIP_IF_INVALID:\n+\t\tcase SIGN_SIGN_IF_INVALID:\n \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n@@ -3011,9 +3063,11 @@ static void parse_new_commit(const char *arg)\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n \n-\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n+\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n+\t     signed_commit_mode == SIGN_SIGN_IF_INVALID) &&\n \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n-\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n+\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n+\t\t\t\t\t    &msg, signed_commit_mode);\n \telse\n \t\tfinalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);\n \n@@ -3060,6 +3114,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\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 \tdefault:\n \t\tBUG(\"invalid signed_tag_mode value %d from tag '%s'\",\n \t\t    signed_tag_mode, name);\n@@ -3649,10 +3706,10 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_commit_mode))\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))\n+\t\tif (parse_sign_mode(option, &signed_tag_mode, NULL))\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/gpg-interface.c b/gpg-interface.c\nindex ce935908cc..c26bd32120 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1151,21 +1151,28 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \treturn ret;\n }\n \n-int parse_sign_mode(const char *arg, enum sign_mode *mode)\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)\n {\n-\tif (!strcmp(arg, \"abort\"))\n+\tif (!strcmp(arg, \"abort\")) {\n \t\t*mode = SIGN_ABORT;\n-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n+\t} else if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\")) {\n \t\t*mode = SIGN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n+\t} else if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\")) {\n \t\t*mode = SIGN_WARN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-strip\"))\n+\t} else if (!strcmp(arg, \"warn-strip\")) {\n \t\t*mode = SIGN_WARN_STRIP;\n-\telse if (!strcmp(arg, \"strip\"))\n+\t} else if (!strcmp(arg, \"strip\")) {\n \t\t*mode = SIGN_STRIP;\n-\telse if (!strcmp(arg, \"strip-if-invalid\"))\n+\t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n \t\t*mode = SIGN_STRIP_IF_INVALID;\n-\telse\n+\t} else if (!strcmp(arg, \"sign-if-invalid\")) {\n+\t\t*mode = SIGN_SIGN_IF_INVALID;\n+\t} else if (skip_prefix(arg, \"sign-if-invalid=\", &arg)) {\n+\t\t*mode = SIGN_SIGN_IF_INVALID;\n+\t\tif (keyid)\n+\t\t\t*keyid = arg;\n+\t} else {\n \t\treturn -1;\n+\t}\n \treturn 0;\n }\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 37f3ac42db..a365586ce1 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -120,12 +120,15 @@ enum sign_mode {\n \tSIGN_WARN_STRIP,\n \tSIGN_STRIP,\n \tSIGN_STRIP_IF_INVALID,\n+\tSIGN_SIGN_IF_INVALID,\n };\n \n /*\n  * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n- * otherwise.\n+ * otherwise. If the parsed mode is SIGN_SIGN_IF_INVALID and GPG key provided in\n+ * the arguments in the form `sign-if-invalid=<keyid>`, the key-ID is parsed\n+ * into `char **keyid`.\n  */\n-int parse_sign_mode(const char *arg, enum sign_mode *mode);\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);\n \n #endif\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nindex 022dae02e4..38b3e3b537 100755\n--- a/t/t9305-fast-import-signatures.sh\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -103,26 +103,85 @@ test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war\n \ttest_line_count = 2 out\n '\n \n-test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '\n-\tgit fast-export main >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n+for mode in strip-if-invalid sign-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+\t\tgit -C new fast-import --quiet --signed-commits=$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-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\t\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"handle signature invalidated by message change with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\n+\t\t# Change the commit message, which invalidates the signature.\n+\t\t# The commit message length should not change though, otherwise the\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\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+\t\ttest $OPENPGP_SIGNING != $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\n+\t\tif test \"$mode\" = strip-if-invalid\n+\t\tthen\n+\t\t\ttest_grep \"stripping invalid signature\" log &&\n+\t\t\ttest_grep ! -E \"^gpgsig\" actual\n+\t\telse\n+\t\t\ttest_grep \"signing commit with invalid signature\" log &&\n+\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\t\tgit -C new verify-commit \"$IMPORTED\"\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success GPGSM \"keep valid X.509 signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\t\ttest $X509_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSSH \"keep valid SSH signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\t\ttest $SSH_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+done\n+\n+test_expect_success GPGSSH \"sign invalid commit with explicit keyid\" '\n \trm -rf new &&\n \tgit init new &&\n \n@@ -133,41 +192,22 @@ test_expect_success GPG 'strip signature invalidated by message change with --si\n \t# corresponding `data <length>` command would have to be changed too.\n \tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n \n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n+\t# Configure the target repository with an invalid default signing key.\n+\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n+\ttest_config -C new gpg.format ssh &&\n+\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git -C new fast-import --quiet \\\n+\t\t--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&\n+\n+\t# Import using explicitly provided signing key.\n+\tgit -C new fast-import --quiet \\\n+\t\t--signed-commits=sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n \n \tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n \ttest $OPENPGP_SIGNING != $IMPORTED &&\n \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep ! -E \"^gpgsig\" actual &&\n-\ttest_grep \"stripping invalid signature\" log\n-'\n-\n-test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n-\ttest $X509_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n-\n-\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n-\ttest $SSH_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n+\tgit -C new verify-commit \"$IMPORTED\"\n '\n \n test_done\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538807","messageId":"xmqqtsukdclr.fsf@gitster.g","threadId":"65062","inReplyTo":"20260312192228.481134-1-jltobler@gmail.com","subject":"Re: [PATCH v5 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-12T20:20:00Z","receivedAt":"2026-03-12T20:20:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> Changes since V4:\n> - Instead of introducing a separate `sign_buffer_with_key()` helper,\n>   extend `sign_buffer()` to support a SIGN_BUFFER_USE_DEFAULT_KEY flag.\n> - Fixed message in die().\n\n\nI left small comments on two patches, but everything looks quite\nwell done to me in this iteration.\n"},{"id":"538808","messageId":"xmqqsea4dclc.fsf@gitster.g","threadId":"65062","inReplyTo":"20260312192228.481134-3-jltobler@gmail.com","subject":"Re: [PATCH v5 2/3] gpg-interface: allow sign_buffer() to use default signing key","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-12T20:20:15Z","receivedAt":"2026-03-12T20:20:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> -\treturn use_format->sign_buffer(buffer, signature, signing_key);\n> +\tif (flags & SIGN_BUFFER_USE_DEFAULT_KEY && (!signing_key || !*signing_key))\n> +\t\tsigning_key = keyid_to_free = get_signing_key();\n\nMicronit.\n\nI would have preferred to see an extra pair of parentheses here, i.e.,\n\n\tif ((flags & SIGN_BUFFER_USE_DEFAULT_KEY) &&\n\t    (!signing_key || !*signing_key))\n                    \nIt would make it more obvious what two conditions are required to\nenter the body, even to those who well know the operator precedence\nrules between & and &&.\n\n"},{"id":"538809","messageId":"xmqqqzpodcl4.fsf@gitster.g","threadId":"65062","inReplyTo":"20260312192228.481134-4-jltobler@gmail.com","subject":"Re: [PATCH v5 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-12T20:20:23Z","receivedAt":"2026-03-12T20:20:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> +\tcase SIGN_SIGN_IF_INVALID:\n> +\t\tif (subject_len > 100)\n> +\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n> +\t\telse if (subject_len > 0)\n> +\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n> +\t\telse\n> +\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), signer);\n\nA very minor point, but my reading hiccuped around these messages,\nsounding as if we are adding an invalid signature to the commit.\n\nPerhaps \"replacing an invalid signature for commit\" or \"re-signing\ncommit that has an invalid signature\" or along that lines would\nreduce the chance of confusion?\n"},{"id":"538810","messageId":"abMguEa0Lmga19Dr@denethor","threadId":"65062","inReplyTo":"xmqqsea4dclc.fsf@gitster.g","subject":"Re: [PATCH v5 2/3] gpg-interface: allow sign_buffer() to use default signing key","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T20:24:34Z","receivedAt":"2026-03-12T20:24:35Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/12 01:20PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > -\treturn use_format->sign_buffer(buffer, signature, signing_key);\n> > +\tif (flags & SIGN_BUFFER_USE_DEFAULT_KEY && (!signing_key || !*signing_key))\n> > +\t\tsigning_key = keyid_to_free = get_signing_key();\n> \n> Micronit.\n> \n> I would have preferred to see an extra pair of parentheses here, i.e.,\n> \n> \tif ((flags & SIGN_BUFFER_USE_DEFAULT_KEY) &&\n> \t    (!signing_key || !*signing_key))\n>                     \n> It would make it more obvious what two conditions are required to\n> enter the body, even to those who well know the operator precedence\n> rules between & and &&.\n\nThat's completely fair. I don't mind fixing and sending another version.\n\nThanks,\n-Justin\n"},{"id":"538811","messageId":"abMhBABG4OpRsjsh@denethor","threadId":"65062","inReplyTo":"xmqqqzpodcl4.fsf@gitster.g","subject":"Re: [PATCH v5 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T20:29:52Z","receivedAt":"2026-03-12T20:29:54Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/12 01:20PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > +\tcase SIGN_SIGN_IF_INVALID:\n> > +\t\tif (subject_len > 100)\n> > +\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n> > +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n> > +\t\telse if (subject_len > 0)\n> > +\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n> > +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n> > +\t\telse\n> > +\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n> > +\t\t\t\t  \"  allegedly by %s\"), signer);\n> \n> A very minor point, but my reading hiccuped around these messages,\n> sounding as if we are adding an invalid signature to the commit.\n> \n> Perhaps \"replacing an invalid signature for commit\" or \"re-signing\n> commit that has an invalid signature\" or along that lines would\n> reduce the chance of confusion?\n\nYa, maybe \"replacing invalid signature for commit ...\" would be better.\nI know Patrick is suggesting we consider getting rid of these warning\nmessages altogether in a followup series. For now though, I'll update it\nin the next version.\n\nThanks,\n-Justin\n"},{"id":"538812","messageId":"abMiT6J7M2BK9jKi@denethor","threadId":"65062","inReplyTo":"xmqqtsukdclr.fsf@gitster.g","subject":"Re: [PATCH v5 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-12T20:30:52Z","receivedAt":"2026-03-12T20:30:54Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/12 01:20PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > Changes since V4:\n> > - Instead of introducing a separate `sign_buffer_with_key()` helper,\n> >   extend `sign_buffer()` to support a SIGN_BUFFER_USE_DEFAULT_KEY flag.\n> > - Fixed message in die().\n> \n> I left small comments on two patches, but everything looks quite\n> well done to me in this iteration.\n\nThanks, I've amended locally and will sent another version a little bit\nlatter today. :)\n\n-Justin\n"},{"id":"538823","messageId":"20260312235828.GA3193385@coredump.intra.peff.net","threadId":"65062","inReplyTo":"20260312192228.481134-4-jltobler@gmail.com","subject":"Re: [PATCH v5 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-12T23:58:28Z","receivedAt":"2026-03-12T23:58:31Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Mar 12, 2026 at 02:22:28PM -0500, Justin Tobler wrote:\n\n> +test_expect_success GPGSSH \"sign invalid commit with explicit keyid\" '\n>  \trm -rf new &&\n>  \tgit init new &&\n\nThis test is failing on Windows in GitHub's CI.\n\nYou can't see it from the context, but the next line of this test is:\n\n\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n\nBut the openpgp-signing branch will only have been created if the GPG\nprereq is set. Should this be referencing ssh-signing instead? Or should\nit be using GPG,GPGSSH as prereqs?\n\n-Peff\n"},{"id":"538826","messageId":"abNXLWadH-nUQRpS@denethor","threadId":"65062","inReplyTo":"20260312235828.GA3193385@coredump.intra.peff.net","subject":"Re: [PATCH v5 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-13T00:17:08Z","receivedAt":"2026-03-13T00:17:10Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/12 07:58PM, Jeff King wrote:\n> On Thu, Mar 12, 2026 at 02:22:28PM -0500, Justin Tobler wrote:\n> \n> > +test_expect_success GPGSSH \"sign invalid commit with explicit keyid\" '\n> >  \trm -rf new &&\n> >  \tgit init new &&\n> \n> This test is failing on Windows in GitHub's CI.\n> \n> You can't see it from the context, but the next line of this test is:\n> \n> \tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n> \n> But the openpgp-signing branch will only have been created if the GPG\n> prereq is set. Should this be referencing ssh-signing instead? Or should\n> it be using GPG,GPGSSH as prereqs?\n\nAhh, it look like this should be referencing ssh-signing. I'll include\nthis fix in my next version. Thanks! :)\n\n-Justin\n"},{"id":"538830","messageId":"20260313013938.2742124-1-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260312192228.481134-1-jltobler@gmail.com","subject":"[PATCH v6 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-13T01:39:35Z","receivedAt":"2026-03-13T01:39:45Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"Greetings,\n\nWith c20f112e51 (fast-import: add 'strip-if-invalid' mode to\n--signed-commits=<mode>, 2025-11-17), it became possible to remove\ninvalid signatures from commits via git-fast-import(1) while maintaining\nvalid commit signatures. Building upon this functionality, a user may\nwant to re-sign these invalid commit signatures. This series introduces\nthe `sign-if-invalid` mode to do so accordingly.\n\nThe newly added mode in this series currently ignores\n`extensions.compatObjectFormat` when generating the new signatures. From\nmy understanding, to generate the compatibility structure would also\nrequire us to reconstruct the compatibility object for the object being\nsigned. I think this would be possible to do, but would require getting\nthe mapped OIDs for the commit parents and tree. I'm not completely sure\nof a good way to go about this yet though. I'm also not completely\ncertain if this is something that should be addressed as part of this\nseries, or could be done later down the road. So for now I've opted to\ndelay its implementation. I'm open going down the other route if that is\npreferred though.\n\nThe first commit is a simple cleanup for something I noticed while\nreading though commit signing code. The second commit actually\nintroduces the new `--signed-commits` mode.\n\nChanges since V5:\n- Fixed a test that was incorrectly referencing the openpgp-signing\n  branch when it should be using the ssh-signing branch.\n- Changed warning message wording.\n- Added some parentheses in a conditional statement to clarify operation\n  order.\n\nChanges since V4:\n- Instead of introducing a separate `sign_buffer_with_key()` helper,\n  extend `sign_buffer()` to support a SIGN_BUFFER_USE_DEFAULT_KEY flag.\n- Fixed message in die().\n\nChanges since V3:\n- Rename the `re-sign-if-invalid` mode to `sign-if-invalid`. The\n  \"if-invalid\" already implies the signatures was previously signed\n  making \"re-sign\" redundant.\n\nChanges since V2:\n- Adapted commit message in second patch to improve clarity.\n- Fixed typos.\n- Renamed SIGN_RESIGN_IF_INVALID to SIGN_RE_SIGN_IF_INVALID.\n- Created separate helper function to handle printing invalid signature\n  warnings.\n\nChanges since V1:\n- Improved commit messages and comments to better explain why\n  interoperability mode is not currently supported.\n- Clarified documentation for re-sign-if-invalid mode.\n- Renamed `handle_invalid_signature()` to `handle_signature_if_invalid()`.\n- Added warning messages specific to commit resigning.\n- Fixed some small typos.\n- Added support for explicitly specifying the signing key ID via\n  `--signed-commits=re-sign-if-invalid[=<keyid>]` similar to how it can\n  specified in git-commit(1).\n- We now die() as unsupported when attempting to re-sign an invalid\n  commit signature in interoperability mode.\n- We now die() when failing to re-sign a commit.\n\nThanks,\n-Justin\n\nJustin Tobler (3):\n  commit: remove unused forward declaration\n  gpg-interface: allow sign_buffer() to use default signing key\n  fast-import: add mode to sign commits with invalid signatures\n\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 101 +++++++++++++++-----\n builtin/tag.c                      |   4 +-\n commit.c                           |  19 +---\n commit.h                           |   2 -\n gpg-interface.c                    |  36 ++++++--\n gpg-interface.h                    |  19 +++-\n send-pack.c                        |   2 +-\n t/t9305-fast-import-signatures.sh  | 144 ++++++++++++++++++-----------\n 10 files changed, 231 insertions(+), 108 deletions(-)\n\nRange-diff against v5:\n1:  0d00b72ee0 = 1:  0d00b72ee0 commit: remove unused forward declaration\n2:  7a0deed77b ! 2:  5224c1766f gpg-interface: allow sign_buffer() to use default signing key\n    @@ gpg-interface.c: const char *gpg_trust_level_to_str(enum signature_trust_level l\n      \tgpg_interface_lazy_init();\n      \n     -\treturn use_format->sign_buffer(buffer, signature, signing_key);\n    -+\tif (flags & SIGN_BUFFER_USE_DEFAULT_KEY && (!signing_key || !*signing_key))\n    ++\tif ((flags & SIGN_BUFFER_USE_DEFAULT_KEY) && (!signing_key || !*signing_key))\n     +\t\tsigning_key = keyid_to_free = get_signing_key();\n     +\n     +\tret = use_format->sign_buffer(buffer, signature, signing_key);\n3:  e659971e84 ! 3:  d9ad73e05b fast-import: add mode to sign commits with invalid signatures\n    @@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_da\n     +\t\tbreak;\n     +\tcase SIGN_SIGN_IF_INVALID:\n     +\t\tif (subject_len > 100)\n    -+\t\t\twarning(_(\"signing commit with invalid signature for '%.100s...'\\n\"\n    ++\t\t\twarning(_(\"replacing invalid signature for commit '%.100s...'\\n\"\n     +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n     +\t\telse if (subject_len > 0)\n    -+\t\t\twarning(_(\"signing commit with invalid signature for '%.*s'\\n\"\n    ++\t\t\twarning(_(\"replacing invalid signature for commit '%.*s'\\n\"\n     +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n     +\t\telse\n    -+\t\t\twarning(_(\"signing commit with invalid signature\\n\"\n    ++\t\t\twarning(_(\"replacing invalid signature for commit\\n\"\n     +\t\t\t\t  \"  allegedly by %s\"), signer);\n     +\t\tbreak;\n     +\tdefault:\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n     +\t\t\ttest_grep \"stripping invalid signature\" log &&\n     +\t\t\ttest_grep ! -E \"^gpgsig\" actual\n     +\t\telse\n    -+\t\t\ttest_grep \"signing commit with invalid signature\" log &&\n    ++\t\t\ttest_grep \"replacing invalid signature\" log &&\n     +\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n     +\t\t\tgit -C new verify-commit \"$IMPORTED\"\n     +\t\tfi\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s\n      \trm -rf new &&\n      \tgit init new &&\n      \n    -@@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip signature invalidated by message change with --si\n    - \t# corresponding `data <length>` command would have to be changed too.\n    - \tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n    +-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n    ++\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n      \n    + \t# Change the commit message, which invalidates the signature.\n    + \t# The commit message length should not change though, otherwise the\n    + \t# corresponding `data <length>` command would have to be changed too.\n    +-\tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n    +-\n     -\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n    -+\t# Configure the target repository with an invalid default signing key.\n    -+\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n    -+\ttest_config -C new gpg.format ssh &&\n    -+\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n    -+\ttest_must_fail git -C new fast-import --quiet \\\n    -+\t\t--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&\n    -+\n    -+\t# Import using explicitly provided signing key.\n    -+\tgit -C new fast-import --quiet \\\n    -+\t\t--signed-commits=sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n    - \n    - \tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n    - \ttest $OPENPGP_SIGNING != $IMPORTED &&\n    - \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    +-\n    +-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n    +-\ttest $OPENPGP_SIGNING != $IMPORTED &&\n    +-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n     -\ttest_grep ! -E \"^gpgsig\" actual &&\n     -\ttest_grep \"stripping invalid signature\" log\n     -'\n    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip signature inva\n     -\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n     -\ttest $X509_SIGNING = $IMPORTED &&\n     -\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    - \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n    +-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n     -\ttest_must_be_empty log\n     -'\n     -\n     -test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n     -\trm -rf new &&\n     -\tgit init new &&\n    --\n    --\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n    --\n    ++\tsed \"s/SSH signed commit/SSH forged commit/\" output >modified &&\n    + \n    ++\t# Configure the target repository with an invalid default signing key.\n    ++\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n    ++\ttest_config -C new gpg.format ssh &&\n    + \ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n    ++\ttest_must_fail git -C new fast-import --quiet \\\n    ++\t\t--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&\n    ++\n    ++\t# Import using explicitly provided signing key.\n    ++\tgit -C new fast-import --quiet \\\n    ++\t\t--signed-commits=sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n    + \n     -\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n     -\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n    --\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n    + \tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n     -\ttest $SSH_SIGNING = $IMPORTED &&\n    --\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    --\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n    ++\ttest $SSH_SIGNING != $IMPORTED &&\n    + \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n    + \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n     -\ttest_must_be_empty log\n     +\tgit -C new verify-commit \"$IMPORTED\"\n      '\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538831","messageId":"20260313013938.2742124-2-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260313013938.2742124-1-jltobler@gmail.com","subject":"[PATCH v6 1/3] commit: remove unused forward declaration","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-13T01:39:36Z","receivedAt":"2026-03-13T01:39:46Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"In 6206089cbd (commit: write commits for both hashes, 2023-10-01),\n`sign_with_header()` was removed, but its forward declaration in\n\"commit.h\" was left. Remove the unused declaration.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n commit.h | 2 --\n 1 file changed, 2 deletions(-)\n\ndiff --git a/commit.h b/commit.h\nindex 1635de418b..f0c38cb444 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -390,8 +390,6 @@ LAST_ARG_MUST_BE_NULL\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook, const char *name, ...);\n \n-/* Sign a commit or tag buffer, storing the result in a header. */\n-int sign_with_header(struct strbuf *buf, const char *keyid);\n /* Parse the signature out of a header. */\n int parse_buffer_signed_by_header(const char *buffer,\n \t\t\t\t  unsigned long size,\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538832","messageId":"20260313013938.2742124-3-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260313013938.2742124-1-jltobler@gmail.com","subject":"[PATCH v6 2/3] gpg-interface: allow sign_buffer() to use default signing key","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-13T01:39:37Z","receivedAt":"2026-03-13T01:39:48Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\nlogic to get the default configured signing key when a key is not\nprovided and handles generating the commit signature accordingly. This\nsigning operation is not really specific to commits as any arbitrary\nbuffer can be signed. Also, in a subsequent commit, this same logic is\nreused by git-fast-import(1) when signing commits with invalid\nsignatures.\n\nRemove the `sign_commit_to_strbuf()` helper from \"commit.c\" and extend\n`sign_buffer()` in \"gpg-interface.c\" to support using the default key as\na fallback when the `SIGN_BUFFER_USE_DEFAULT_KEY` flag is provided. Call\nsites are updated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/tag.c   |  4 ++--\n commit.c        | 19 +++++--------------\n gpg-interface.c | 13 +++++++++++--\n gpg-interface.h | 12 ++++++++++--\n send-pack.c     |  2 +-\n 5 files changed, 29 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex aeb04c487f..540d783c67 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -167,7 +167,7 @@ static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,\n \tchar *keyid = get_signing_key();\n \tint ret = -1;\n \n-\tif (sign_buffer(buffer, &sig, keyid))\n+\tif (sign_buffer(buffer, &sig, keyid, 0))\n \t\tgoto out;\n \n \tif (compat) {\n@@ -176,7 +176,7 @@ static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,\n \t\tif (convert_object_file(the_repository ,&compat_buf, algo, compat,\n \t\t\t\t\tbuffer->buf, buffer->len, OBJ_TAG, 1))\n \t\t\tgoto out;\n-\t\tif (sign_buffer(&compat_buf, &compat_sig, keyid))\n+\t\tif (sign_buffer(&compat_buf, &compat_sig, keyid, 0))\n \t\t\tgoto out;\n \t\tadd_header_signature(&compat_buf, &sig, algo);\n \t\tstrbuf_addbuf(&compat_buf, &compat_sig);\ndiff --git a/commit.c b/commit.c\nindex d16ae73345..1b9b2d4499 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1148,18 +1148,6 @@ int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi\n \treturn 0;\n }\n \n-static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)\n-{\n-\tchar *keyid_to_free = NULL;\n-\tint ret = 0;\n-\tif (!keyid || !*keyid)\n-\t\tkeyid = keyid_to_free = get_signing_key();\n-\tif (sign_buffer(buf, sig, keyid))\n-\t\tret = -1;\n-\tfree(keyid_to_free);\n-\treturn ret;\n-}\n-\n int parse_signed_commit(const struct commit *commit,\n \t\t\tstruct strbuf *payload, struct strbuf *signature,\n \t\t\tconst struct git_hash_algo *algop)\n@@ -1737,7 +1725,8 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\toidcpy(&parent_buf[i++], &p->item->object.oid);\n \n \twrite_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);\n-\tif (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {\n+\tif (sign_commit && sign_buffer(&buffer, &sig, sign_commit,\n+\t\t\t\t       SIGN_BUFFER_USE_DEFAULT_KEY)) {\n \t\tresult = -1;\n \t\tgoto out;\n \t}\n@@ -1769,7 +1758,9 @@ int commit_tree_extended(const char *msg, size_t msg_len,\n \t\tfree_commit_extra_headers(compat_extra);\n \t\tfree(mapped_parents);\n \n-\t\tif (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {\n+\t\tif (sign_commit && sign_buffer(&compat_buffer, &compat_sig,\n+\t\t\t\t\t       sign_commit,\n+\t\t\t\t\t       SIGN_BUFFER_USE_DEFAULT_KEY)) {\n \t\t\tresult = -1;\n \t\t\tgoto out;\n \t\t}\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 87fb6605fb..dca192d5c4 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -973,11 +973,20 @@ const char *gpg_trust_level_to_str(enum signature_trust_level level)\n \treturn sigcheck_gpg_trust_level[level].display_key;\n }\n \n-int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n+int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n+\t\tconst char *signing_key, enum sign_buffer_flags flags)\n {\n+\tchar *keyid_to_free = NULL;\n+\tint ret = 0;\n+\n \tgpg_interface_lazy_init();\n \n-\treturn use_format->sign_buffer(buffer, signature, signing_key);\n+\tif ((flags & SIGN_BUFFER_USE_DEFAULT_KEY) && (!signing_key || !*signing_key))\n+\t\tsigning_key = keyid_to_free = get_signing_key();\n+\n+\tret = use_format->sign_buffer(buffer, signature, signing_key);\n+\tfree(keyid_to_free);\n+\treturn ret;\n }\n \n /*\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 789d1ffac4..37f3ac42db 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -74,6 +74,15 @@ int parse_signature(const char *buf, size_t size, struct strbuf *payload, struct\n  */\n size_t parse_signed_buffer(const char *buf, size_t size);\n \n+/* Flags for sign_buffer(). */\n+enum sign_buffer_flags {\n+\t/*\n+\t * Use the default configured signing key as returned by `get_signing_key()`\n+\t * when the provided \"signing_key\" is NULL or empty.\n+\t */\n+\tSIGN_BUFFER_USE_DEFAULT_KEY = (1 << 0),\n+};\n+\n /*\n  * Create a detached signature for the contents of \"buffer\" and append\n  * it after \"signature\"; \"buffer\" and \"signature\" can be the same\n@@ -81,8 +90,7 @@ size_t parse_signed_buffer(const char *buf, size_t size);\n  * at the end.  Returns 0 on success, non-zero on failure.\n  */\n int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n-\t\tconst char *signing_key);\n-\n+\t\tconst char *signing_key, enum sign_buffer_flags flags);\n \n /*\n  * Returns corresponding string in lowercase for a given member of\ndiff --git a/send-pack.c b/send-pack.c\nindex 67d6987b1c..07ecfae4de 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -391,7 +391,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, signing_key, 0))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538833","messageId":"20260313013938.2742124-4-jltobler@gmail.com","threadId":"65062","inReplyTo":"20260313013938.2742124-1-jltobler@gmail.com","subject":"[PATCH v6 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-13T01:39:38Z","receivedAt":"2026-03-13T01:39:49Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"With git-fast-import(1), handling of signed commits is controlled via\nthe `--signed-commits=<mode>` option. When an invalid signature is\nencountered, a user may want the option to sign the commit again as\nopposed to just stripping the signature. To facilitate this, introduce a\n\"sign-if-invalid\" mode for the `--signed-commits` option. Optionally, a\nkey ID may be explicitly provided in the form\n`sign-if-invalid[=<keyid>]` to specify which signing key should be used\nwhen signing invalid commit signatures.\n\nNote that to properly support interoperability mode when signing commit\nsignatures, the commit buffer must be created in both the repository and\ncompatability object formats to generate the appropriate signatures\naccordingly. As currently implemented, the commit buffer for the\ncompatability object format is not reconstructed and thus signing\ncommits in interoperability mode is not yet supported. Support may be\nadded in the future.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n Documentation/git-fast-import.adoc |   4 +\n builtin/fast-export.c              |   8 +-\n builtin/fast-import.c              | 101 +++++++++++++++-----\n gpg-interface.c                    |  23 +++--\n gpg-interface.h                    |   7 +-\n t/t9305-fast-import-signatures.sh  | 144 ++++++++++++++++++-----------\n 6 files changed, 202 insertions(+), 85 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 479c4081da..b3f42d4637 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -86,6 +86,10 @@ already trusted to run their own code.\n * `strip-if-invalid` will check signatures and, if they are invalid,\n   will strip them and display a warning. The validation is performed\n   in the same way as linkgit:git-verify-commit[1] does it.\n+* `sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies\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 \n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 0c5d2386d8..13621b0d6a 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -64,7 +64,7 @@ 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))\n+\tif (parse_sign_mode(arg, val, NULL))\n \t\treturn error(_(\"unknown %s mode: %s\"), opt->long_name, arg);\n \n \treturn 0;\n@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\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,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)\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}\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b8a7757cfd..935e688e33 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -190,6 +190,7 @@ static const char *global_prefix;\n \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 \n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n@@ -2836,26 +2837,15 @@ static void finalize_commit_buffer(struct strbuf *new_data,\n \tstrbuf_addbuf(new_data, msg);\n }\n \n-static void handle_strip_if_invalid(struct strbuf *new_data,\n-\t\t\t\t    struct signature_data *sig_sha1,\n-\t\t\t\t    struct signature_data *sig_sha256,\n-\t\t\t\t    struct strbuf *msg)\n+static void warn_invalid_signature(struct signature_check *check,\n+\t\t\t\t   const char *msg, enum sign_mode mode)\n {\n-\tstruct strbuf tmp_buf = STRBUF_INIT;\n-\tstruct signature_check signature_check = { 0 };\n-\tint ret;\n-\n-\t/* Check signature in a temporary commit buffer */\n-\tstrbuf_addbuf(&tmp_buf, new_data);\n-\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n-\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n-\n-\tif (ret) {\n-\t\tconst char *signer = signature_check.signer ?\n-\t\t\tsignature_check.signer : _(\"unknown\");\n-\t\tconst char *subject;\n-\t\tint subject_len = find_commit_subject(msg->buf, &subject);\n+\tconst char *signer = check->signer ? check->signer : _(\"unknown\");\n+\tconst char *subject;\n+\tint subject_len = find_commit_subject(msg, &subject);\n \n+\tswitch (mode) {\n+\tcase SIGN_STRIP_IF_INVALID:\n \t\tif (subject_len > 100)\n \t\t\twarning(_(\"stripping invalid signature for commit '%.100s...'\\n\"\n \t\t\t\t  \"  allegedly by %s\"), subject, signer);\n@@ -2865,6 +2855,67 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n \t\telse\n \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n \t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tcase SIGN_SIGN_IF_INVALID:\n+\t\tif (subject_len > 100)\n+\t\t\twarning(_(\"replacing invalid signature for commit '%.100s...'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n+\t\telse if (subject_len > 0)\n+\t\t\twarning(_(\"replacing invalid signature for commit '%.*s'\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n+\t\telse\n+\t\t\twarning(_(\"replacing invalid signature for commit\\n\"\n+\t\t\t\t  \"  allegedly by %s\"), signer);\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unsupported signing mode\");\n+\t}\n+}\n+\n+static void handle_signature_if_invalid(struct strbuf *new_data,\n+\t\t\t\t\tstruct signature_data *sig_sha1,\n+\t\t\t\t\tstruct signature_data *sig_sha256,\n+\t\t\t\t\tstruct strbuf *msg,\n+\t\t\t\t\tenum sign_mode mode)\n+{\n+\tstruct strbuf tmp_buf = STRBUF_INIT;\n+\tstruct signature_check signature_check = { 0 };\n+\tint ret;\n+\n+\t/* Check signature in a temporary commit buffer */\n+\tstrbuf_addbuf(&tmp_buf, new_data);\n+\tfinalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);\n+\tret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);\n+\n+\tif (ret) {\n+\t\twarn_invalid_signature(&signature_check, msg->buf, mode);\n+\n+\t\tif (mode == SIGN_SIGN_IF_INVALID) {\n+\t\t\tstruct strbuf signature = STRBUF_INIT;\n+\t\t\tstruct strbuf payload = STRBUF_INIT;\n+\n+\t\t\t/*\n+\t\t\t * NEEDSWORK: To properly support interoperability mode\n+\t\t\t * when signing commit signatures, the commit buffer\n+\t\t\t * must be provided in both the repository and\n+\t\t\t * compatibility object formats. As currently\n+\t\t\t * implemented, only the repository object format is\n+\t\t\t * considered meaning compatibility signatures cannot be\n+\t\t\t * generated. Thus, attempting to sign commit signatures\n+\t\t\t * in interoperability mode is currently unsupported.\n+\t\t\t */\n+\t\t\tif (the_repository->compat_hash_algo)\n+\t\t\t\tdie(_(\"signing commits in interoperability mode is unsupported\"));\n+\n+\t\t\tstrbuf_addstr(&payload, signature_check.payload);\n+\t\t\tif (sign_buffer(&payload, &signature, signed_commit_keyid,\n+\t\t\t\t\tSIGN_BUFFER_USE_DEFAULT_KEY))\n+\t\t\t\tdie(_(\"failed to sign commit object\"));\n+\t\t\tadd_header_signature(new_data, &signature, the_hash_algo);\n+\n+\t\t\tstrbuf_release(&signature);\n+\t\t\tstrbuf_release(&payload);\n+\t\t}\n \n \t\tfinalize_commit_buffer(new_data, NULL, NULL, msg);\n \t} else {\n@@ -2927,6 +2978,7 @@ static void parse_new_commit(const char *arg)\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n \t\tcase SIGN_STRIP_IF_INVALID:\n+\t\tcase SIGN_SIGN_IF_INVALID:\n \t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n \t\t\tbreak;\n \n@@ -3011,9 +3063,11 @@ static void parse_new_commit(const char *arg)\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n \n-\tif (signed_commit_mode == SIGN_STRIP_IF_INVALID &&\n+\tif ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||\n+\t     signed_commit_mode == SIGN_SIGN_IF_INVALID) &&\n \t    (sig_sha1.hash_algo || sig_sha256.hash_algo))\n-\t\thandle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);\n+\t\thandle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,\n+\t\t\t\t\t    &msg, signed_commit_mode);\n \telse\n \t\tfinalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);\n \n@@ -3060,6 +3114,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)\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 \tdefault:\n \t\tBUG(\"invalid signed_tag_mode value %d from tag '%s'\",\n \t\t    signed_tag_mode, name);\n@@ -3649,10 +3706,10 @@ static int parse_one_option(const char *option)\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n \t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n-\t\tif (parse_sign_mode(option, &signed_commit_mode))\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))\n+\t\tif (parse_sign_mode(option, &signed_tag_mode, NULL))\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/gpg-interface.c b/gpg-interface.c\nindex dca192d5c4..32f2880976 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1151,21 +1151,28 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \treturn ret;\n }\n \n-int parse_sign_mode(const char *arg, enum sign_mode *mode)\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)\n {\n-\tif (!strcmp(arg, \"abort\"))\n+\tif (!strcmp(arg, \"abort\")) {\n \t\t*mode = SIGN_ABORT;\n-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n+\t} else if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\")) {\n \t\t*mode = SIGN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n+\t} else if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\")) {\n \t\t*mode = SIGN_WARN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-strip\"))\n+\t} else if (!strcmp(arg, \"warn-strip\")) {\n \t\t*mode = SIGN_WARN_STRIP;\n-\telse if (!strcmp(arg, \"strip\"))\n+\t} else if (!strcmp(arg, \"strip\")) {\n \t\t*mode = SIGN_STRIP;\n-\telse if (!strcmp(arg, \"strip-if-invalid\"))\n+\t} else if (!strcmp(arg, \"strip-if-invalid\")) {\n \t\t*mode = SIGN_STRIP_IF_INVALID;\n-\telse\n+\t} else if (!strcmp(arg, \"sign-if-invalid\")) {\n+\t\t*mode = SIGN_SIGN_IF_INVALID;\n+\t} else if (skip_prefix(arg, \"sign-if-invalid=\", &arg)) {\n+\t\t*mode = SIGN_SIGN_IF_INVALID;\n+\t\tif (keyid)\n+\t\t\t*keyid = arg;\n+\t} else {\n \t\treturn -1;\n+\t}\n \treturn 0;\n }\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 37f3ac42db..a365586ce1 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -120,12 +120,15 @@ enum sign_mode {\n \tSIGN_WARN_STRIP,\n \tSIGN_STRIP,\n \tSIGN_STRIP_IF_INVALID,\n+\tSIGN_SIGN_IF_INVALID,\n };\n \n /*\n  * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n- * otherwise.\n+ * otherwise. If the parsed mode is SIGN_SIGN_IF_INVALID and GPG key provided in\n+ * the arguments in the form `sign-if-invalid=<keyid>`, the key-ID is parsed\n+ * into `char **keyid`.\n  */\n-int parse_sign_mode(const char *arg, enum sign_mode *mode);\n+int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);\n \n #endif\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nindex 022dae02e4..ac4228127a 100755\n--- a/t/t9305-fast-import-signatures.sh\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -103,71 +103,111 @@ test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war\n \ttest_line_count = 2 out\n '\n \n-test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '\n-\tgit fast-export main >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '\n+for mode in strip-if-invalid sign-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+\t\tgit -C new fast-import --quiet --signed-commits=$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-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\t\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPG \"handle signature invalidated by message change with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\n+\t\t# Change the commit message, which invalidates the signature.\n+\t\t# The commit message length should not change though, otherwise the\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\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+\t\ttest $OPENPGP_SIGNING != $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\n+\t\tif test \"$mode\" = strip-if-invalid\n+\t\tthen\n+\t\t\ttest_grep \"stripping invalid signature\" log &&\n+\t\t\ttest_grep ! -E \"^gpgsig\" actual\n+\t\telse\n+\t\t\ttest_grep \"replacing invalid signature\" log &&\n+\t\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\t\tgit -C new verify-commit \"$IMPORTED\"\n+\t\tfi\n+\t'\n+\n+\ttest_expect_success GPGSM \"keep valid X.509 signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\t\ttest $X509_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+\n+\ttest_expect_success GPGSSH \"keep valid SSH signature with --signed-commits=$mode\" '\n+\t\trm -rf new &&\n+\t\tgit init new &&\n+\n+\t\ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\t\tgit -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&\n+\t\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\t\ttest $SSH_SIGNING = $IMPORTED &&\n+\t\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\t\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n+\t\ttest_must_be_empty log\n+\t'\n+done\n+\n+test_expect_success GPGSSH \"sign invalid commit with explicit keyid\" '\n \trm -rf new &&\n \tgit init new &&\n \n-\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n \n \t# Change the commit message, which invalidates the signature.\n \t# The commit message length should not change though, otherwise the\n \t# corresponding `data <length>` command would have to be changed too.\n-\tsed \"s/OpenPGP signed commit/OpenPGP forged commit/\" output >modified &&\n-\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&\n-\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n-\ttest $OPENPGP_SIGNING != $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep ! -E \"^gpgsig\" actual &&\n-\ttest_grep \"stripping invalid signature\" log\n-'\n-\n-test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n-\n-\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n-\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n-\ttest $X509_SIGNING = $IMPORTED &&\n-\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n-\ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n-'\n-\n-test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '\n-\trm -rf new &&\n-\tgit init new &&\n+\tsed \"s/SSH signed commit/SSH forged commit/\" output >modified &&\n \n+\t# Configure the target repository with an invalid default signing key.\n+\ttest_config -C new user.signingkey \"not-a-real-key-id\" &&\n+\ttest_config -C new gpg.format ssh &&\n \ttest_config -C new gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git -C new fast-import --quiet \\\n+\t\t--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&\n+\n+\t# Import using explicitly provided signing key.\n+\tgit -C new fast-import --quiet \\\n+\t\t--signed-commits=sign-if-invalid=\"${GPGSSH_KEY_PRIMARY}\" <modified &&\n \n-\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n-\tgit -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&\n \tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n-\ttest $SSH_SIGNING = $IMPORTED &&\n+\ttest $SSH_SIGNING != $IMPORTED &&\n \tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n \ttest_grep -E \"^gpgsig(-sha256)? \" actual &&\n-\ttest_must_be_empty log\n+\tgit -C new verify-commit \"$IMPORTED\"\n '\n \n test_done\n-- \n2.53.0.381.g628a66ccf6\n\n"},{"id":"538841","messageId":"xmqqy0jw8i8e.fsf@gitster.g","threadId":"65062","inReplyTo":"20260313013938.2742124-1-jltobler@gmail.com","subject":"Re: [PATCH v6 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-13T04:29:37Z","receivedAt":"2026-03-13T04:29:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> Changes since V5:\n> - Fixed a test that was incorrectly referencing the openpgp-signing\n>   branch when it should be using the ssh-signing branch.\n> - Changed warning message wording.\n> - Added some parentheses in a conditional statement to clarify operation\n>   order.\n\nAll three of the above look familiar ;-)  Looking good.\n\nWill replace.  Thanks.\n"},{"id":"538849","messageId":"abOvUyHiMO9leA9O@pks.im","threadId":"65062","inReplyTo":"20260313013938.2742124-3-jltobler@gmail.com","subject":"Re: [PATCH v6 2/3] gpg-interface: allow sign_buffer() to use default signing key","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-13T06:31:47Z","receivedAt":"2026-03-13T06:31:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Mar 12, 2026 at 08:39:37PM -0500, Justin Tobler wrote:\n> The `sign_commit_to_strbuf()` helper in \"commit.c\" provides fallback\n> logic to get the default configured signing key when a key is not\n> provided and handles generating the commit signature accordingly. This\n> signing operation is not really specific to commits as any arbitrary\n> buffer can be signed. Also, in a subsequent commit, this same logic is\n> reused by git-fast-import(1) when signing commits with invalid\n> signatures.\n> \n> Remove the `sign_commit_to_strbuf()` helper from \"commit.c\" and extend\n> `sign_buffer()` in \"gpg-interface.c\" to support using the default key as\n> a fallback when the `SIGN_BUFFER_USE_DEFAULT_KEY` flag is provided. Call\n> sites are updated accordingly.\n\nThanks, this looks much nicer to me now.\n\nPatrick\n"},{"id":"538850","messageId":"abOvWQojOvkJh7QP@pks.im","threadId":"65062","inReplyTo":"20260313013938.2742124-4-jltobler@gmail.com","subject":"Re: [PATCH v6 3/3] fast-import: add mode to sign commits with invalid signatures","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-13T06:31:53Z","receivedAt":"2026-03-13T06:31:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Mar 12, 2026 at 08:39:38PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index b8a7757cfd..935e688e33 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -2865,6 +2855,67 @@ static void handle_strip_if_invalid(struct strbuf *new_data,\n>  \t\telse\n>  \t\t\twarning(_(\"stripping invalid signature for commit\\n\"\n>  \t\t\t\t  \"  allegedly by %s\"), signer);\n> +\t\tbreak;\n> +\tcase SIGN_SIGN_IF_INVALID:\n> +\t\tif (subject_len > 100)\n> +\t\t\twarning(_(\"replacing invalid signature for commit '%.100s...'\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), subject, signer);\n> +\t\telse if (subject_len > 0)\n> +\t\t\twarning(_(\"replacing invalid signature for commit '%.*s'\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), subject_len, subject, signer);\n> +\t\telse\n> +\t\t\twarning(_(\"replacing invalid signature for commit\\n\"\n> +\t\t\t\t  \"  allegedly by %s\"), signer);\n> +\t\tbreak;\n> +\tdefault:\n> +\t\tBUG(\"unsupported signing mode\");\n> +\t}\n> +}\n\nThe wording of those warnings also reads better than before now.\n\nPatrick\n"},{"id":"538851","messageId":"abOvXzwDLppM1Rzn@pks.im","threadId":"65062","inReplyTo":"xmqqy0jw8i8e.fsf@gitster.g","subject":"Re: [PATCH v6 0/3] fast-import: add mode to re-sign invalid commit signatures","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-13T06:31:59Z","receivedAt":"2026-03-13T06:32:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Mar 12, 2026 at 09:29:37PM -0700, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > Changes since V5:\n> > - Fixed a test that was incorrectly referencing the openpgp-signing\n> >   branch when it should be using the ssh-signing branch.\n> > - Changed warning message wording.\n> > - Added some parentheses in a conditional statement to clarify operation\n> >   order.\n> \n> All three of the above look familiar ;-)  Looking good.\n> \n> Will replace.  Thanks.\n\nI'm happy with this version. Thanks!\n\nPatrick\n"}]}