Subject: [PATCH v6 0/3] fast-import: add mode to re-sign invalid commit signatures

From: Justin Tobler <[email-masked]>
Date: Thu, 12 Mar 2026 20:39:35 -0500

Greetings,

With c20f112e51 (fast-import: add 'strip-if-invalid' mode to
--signed-commits=<mode>, 2025-11-17), it became possible to remove
invalid signatures from commits via git-fast-import(1) while maintaining
valid commit signatures. Building upon this functionality, a user may
want to re-sign these invalid commit signatures. This series introduces
the `sign-if-invalid` mode to do so accordingly.

The newly added mode in this series currently ignores
`extensions.compatObjectFormat` when generating the new signatures. From
my understanding, to generate the compatibility structure would also
require us to reconstruct the compatibility object for the object being
signed. I think this would be possible to do, but would require getting
the mapped OIDs for the commit parents and tree. I'm not completely sure
of a good way to go about this yet though. I'm also not completely
certain if this is something that should be addressed as part of this
series, or could be done later down the road. So for now I've opted to
delay its implementation. I'm open going down the other route if that is
preferred though.

The first commit is a simple cleanup for something I noticed while
reading though commit signing code. The second commit actually
introduces the new `--signed-commits` mode.

Changes since V5:
- Fixed a test that was incorrectly referencing the openpgp-signing
  branch when it should be using the ssh-signing branch.
- Changed warning message wording.
- Added some parentheses in a conditional statement to clarify operation
  order.

Changes since V4:
- Instead of introducing a separate `sign_buffer_with_key()` helper,
  extend `sign_buffer()` to support a SIGN_BUFFER_USE_DEFAULT_KEY flag.
- Fixed message in die().

Changes since V3:
- Rename the `re-sign-if-invalid` mode to `sign-if-invalid`. The
  "if-invalid" already implies the signatures was previously signed
  making "re-sign" redundant.

Changes since V2:
- Adapted commit message in second patch to improve clarity.
- Fixed typos.
- Renamed SIGN_RESIGN_IF_INVALID to SIGN_RE_SIGN_IF_INVALID.
- Created separate helper function to handle printing invalid signature
  warnings.

Changes since V1:
- Improved commit messages and comments to better explain why
  interoperability mode is not currently supported.
- Clarified documentation for re-sign-if-invalid mode.
- Renamed `handle_invalid_signature()` to `handle_signature_if_invalid()`.
- Added warning messages specific to commit resigning.
- Fixed some small typos.
- Added support for explicitly specifying the signing key ID via
  `--signed-commits=re-sign-if-invalid[=<keyid>]` similar to how it can
  specified in git-commit(1).
- We now die() as unsupported when attempting to re-sign an invalid
  commit signature in interoperability mode.
- We now die() when failing to re-sign a commit.

Thanks,
-Justin

Justin Tobler (3):
  commit: remove unused forward declaration
  gpg-interface: allow sign_buffer() to use default signing key
  fast-import: add mode to sign commits with invalid signatures

 Documentation/git-fast-import.adoc |   4 +
 builtin/fast-export.c              |   8 +-
 builtin/fast-import.c              | 101 +++++++++++++++-----
 builtin/tag.c                      |   4 +-
 commit.c                           |  19 +---
 commit.h                           |   2 -
 gpg-interface.c                    |  36 ++++++--
 gpg-interface.h                    |  19 +++-
 send-pack.c                        |   2 +-
 t/t9305-fast-import-signatures.sh  | 144 ++++++++++++++++++-----------
 10 files changed, 231 insertions(+), 108 deletions(-)

Range-diff against v5:
1:  0d00b72ee0 = 1:  0d00b72ee0 commit: remove unused forward declaration
2:  7a0deed77b ! 2:  5224c1766f gpg-interface: allow sign_buffer() to use default signing key
    @@ gpg-interface.c: const char *gpg_trust_level_to_str(enum signature_trust_level l
      	gpg_interface_lazy_init();
      
     -	return use_format->sign_buffer(buffer, signature, signing_key);
    -+	if (flags & SIGN_BUFFER_USE_DEFAULT_KEY && (!signing_key || !*signing_key))
    ++	if ((flags & SIGN_BUFFER_USE_DEFAULT_KEY) && (!signing_key || !*signing_key))
     +		signing_key = keyid_to_free = get_signing_key();
     +
     +	ret = use_format->sign_buffer(buffer, signature, signing_key);
3:  e659971e84 ! 3:  d9ad73e05b fast-import: add mode to sign commits with invalid signatures
    @@ builtin/fast-import.c: static void handle_strip_if_invalid(struct strbuf *new_da
     +		break;
     +	case SIGN_SIGN_IF_INVALID:
     +		if (subject_len > 100)
    -+			warning(_("signing commit with invalid signature for '%.100s...'\n"
    ++			warning(_("replacing invalid signature for commit '%.100s...'\n"
     +				  "  allegedly by %s"), subject, signer);
     +		else if (subject_len > 0)
    -+			warning(_("signing commit with invalid signature for '%.*s'\n"
    ++			warning(_("replacing invalid signature for commit '%.*s'\n"
     +				  "  allegedly by %s"), subject_len, subject, signer);
     +		else
    -+			warning(_("signing commit with invalid signature\n"
    ++			warning(_("replacing invalid signature for commit\n"
     +				  "  allegedly by %s"), signer);
     +		break;
     +	default:
    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s
     +			test_grep "stripping invalid signature" log &&
     +			test_grep ! -E "^gpgsig" actual
     +		else
    -+			test_grep "signing commit with invalid signature" log &&
    ++			test_grep "replacing invalid signature" log &&
     +			test_grep -E "^gpgsig(-sha256)? " actual &&
     +			git -C new verify-commit "$IMPORTED"
     +		fi
    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip both OpenPGP s
      	rm -rf new &&
      	git init new &&
      
    -@@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip signature invalidated by message change with --si
    - 	# corresponding `data <length>` command would have to be changed too.
    - 	sed "s/OpenPGP signed commit/OpenPGP forged commit/" output >modified &&
    +-	git fast-export --signed-commits=verbatim openpgp-signing >output &&
    ++	git fast-export --signed-commits=verbatim ssh-signing >output &&
      
    + 	# Change the commit message, which invalidates the signature.
    + 	# The commit message length should not change though, otherwise the
    + 	# corresponding `data <length>` command would have to be changed too.
    +-	sed "s/OpenPGP signed commit/OpenPGP forged commit/" output >modified &&
    +-
     -	git -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&
    -+	# Configure the target repository with an invalid default signing key.
    -+	test_config -C new user.signingkey "not-a-real-key-id" &&
    -+	test_config -C new gpg.format ssh &&
    -+	test_config -C new gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
    -+	test_must_fail git -C new fast-import --quiet \
    -+		--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&
    -+
    -+	# Import using explicitly provided signing key.
    -+	git -C new fast-import --quiet \
    -+		--signed-commits=sign-if-invalid="${GPGSSH_KEY_PRIMARY}" <modified &&
    - 
    - 	IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
    - 	test $OPENPGP_SIGNING != $IMPORTED &&
    - 	git -C new cat-file commit "$IMPORTED" >actual &&
    +-
    +-	IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
    +-	test $OPENPGP_SIGNING != $IMPORTED &&
    +-	git -C new cat-file commit "$IMPORTED" >actual &&
     -	test_grep ! -E "^gpgsig" actual &&
     -	test_grep "stripping invalid signature" log
     -'
    @@ t/t9305-fast-import-signatures.sh: test_expect_success GPG 'strip signature inva
     -	IMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&
     -	test $X509_SIGNING = $IMPORTED &&
     -	git -C new cat-file commit "$IMPORTED" >actual &&
    - 	test_grep -E "^gpgsig(-sha256)? " actual &&
    +-	test_grep -E "^gpgsig(-sha256)? " actual &&
     -	test_must_be_empty log
     -'
     -
     -test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '
     -	rm -rf new &&
     -	git init new &&
    --
    --	test_config -C new gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
    --
    ++	sed "s/SSH signed commit/SSH forged commit/" output >modified &&
    + 
    ++	# Configure the target repository with an invalid default signing key.
    ++	test_config -C new user.signingkey "not-a-real-key-id" &&
    ++	test_config -C new gpg.format ssh &&
    + 	test_config -C new gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
    ++	test_must_fail git -C new fast-import --quiet \
    ++		--signed-commits=sign-if-invalid <modified >/dev/null 2>&1 &&
    ++
    ++	# Import using explicitly provided signing key.
    ++	git -C new fast-import --quiet \
    ++		--signed-commits=sign-if-invalid="${GPGSSH_KEY_PRIMARY}" <modified &&
    + 
     -	git fast-export --signed-commits=verbatim ssh-signing >output &&
     -	git -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&
    --	IMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&
    + 	IMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&
     -	test $SSH_SIGNING = $IMPORTED &&
    --	git -C new cat-file commit "$IMPORTED" >actual &&
    --	test_grep -E "^gpgsig(-sha256)? " actual &&
    ++	test $SSH_SIGNING != $IMPORTED &&
    + 	git -C new cat-file commit "$IMPORTED" >actual &&
    + 	test_grep -E "^gpgsig(-sha256)? " actual &&
     -	test_must_be_empty log
     +	git -C new verify-commit "$IMPORTED"
      '

base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4
-- 
2.53.0.381.g628a66ccf6