Merge branch 'jt/fast-import-sign-again' into next

"git fast-import" learned to optionally replace signature on
commits whose signatures get invalidated due to replaying by
signing afresh.

* jt/fast-import-sign-again:
  fast-import: add mode to sign commits with invalid signatures
  gpg-interface: allow sign_buffer() to use default signing key
  commit: remove unused forward declaration
This commit is contained in:
Junio C Hamano
2026-03-17 11:32:42 -07:00
10 changed files with 229 additions and 106 deletions

View File

@@ -86,6 +86,10 @@ already trusted to run their own code.
* `strip-if-invalid` will check signatures and, if they are invalid,
will strip them and display a warning. The validation is performed
in the same way as linkgit:git-verify-commit[1] does it.
* `sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies
commit signatures and replaces invalid signatures with newly created ones.
Valid signatures are left unchanged. If `<keyid>` is provided, that key is
used for signing; otherwise the configured default signing key is used.
Options for Frontends
~~~~~~~~~~~~~~~~~~~~~

View File

@@ -64,7 +64,7 @@ static int parse_opt_sign_mode(const struct option *opt,
if (unset)
return 0;
if (parse_sign_mode(arg, val))
if (parse_sign_mode(arg, val, NULL))
return error(_("unknown %s mode: %s"), opt->long_name, arg);
return 0;
@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
case SIGN_STRIP_IF_INVALID:
die(_("'strip-if-invalid' is not a valid mode for "
"git fast-export with --signed-commits=<mode>"));
case SIGN_SIGN_IF_INVALID:
die(_("'sign-if-invalid' is not a valid mode for "
"git fast-export with --signed-commits=<mode>"));
default:
BUG("invalid signed_commit_mode value %d", signed_commit_mode);
}
@@ -970,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)
case SIGN_STRIP_IF_INVALID:
die(_("'strip-if-invalid' is not a valid mode for "
"git fast-export with --signed-tags=<mode>"));
case SIGN_SIGN_IF_INVALID:
die(_("'sign-if-invalid' is not a valid mode for "
"git fast-export with --signed-tags=<mode>"));
default:
BUG("invalid signed_commit_mode value %d", signed_commit_mode);
}

View File

@@ -190,6 +190,7 @@ static const char *global_prefix;
static enum sign_mode signed_tag_mode = SIGN_VERBATIM;
static enum sign_mode signed_commit_mode = SIGN_VERBATIM;
static const char *signed_commit_keyid;
/* Memory pools */
static struct mem_pool fi_mem_pool = {
@@ -2840,10 +2841,46 @@ static void finalize_commit_buffer(struct strbuf *new_data,
strbuf_addbuf(new_data, msg);
}
static void handle_strip_if_invalid(struct strbuf *new_data,
struct signature_data *sig_sha1,
struct signature_data *sig_sha256,
struct strbuf *msg)
static void warn_invalid_signature(struct signature_check *check,
const char *msg, enum sign_mode mode)
{
const char *signer = check->signer ? check->signer : _("unknown");
const char *subject;
int subject_len = find_commit_subject(msg, &subject);
switch (mode) {
case SIGN_STRIP_IF_INVALID:
if (subject_len > 100)
warning(_("stripping invalid signature for commit '%.100s...'\n"
" allegedly by %s"), subject, signer);
else if (subject_len > 0)
warning(_("stripping invalid signature for commit '%.*s'\n"
" allegedly by %s"), subject_len, subject, signer);
else
warning(_("stripping invalid signature for commit\n"
" allegedly by %s"), signer);
break;
case SIGN_SIGN_IF_INVALID:
if (subject_len > 100)
warning(_("replacing invalid signature for commit '%.100s...'\n"
" allegedly by %s"), subject, signer);
else if (subject_len > 0)
warning(_("replacing invalid signature for commit '%.*s'\n"
" allegedly by %s"), subject_len, subject, signer);
else
warning(_("replacing invalid signature for commit\n"
" allegedly by %s"), signer);
break;
default:
BUG("unsupported signing mode");
}
}
static void handle_signature_if_invalid(struct strbuf *new_data,
struct signature_data *sig_sha1,
struct signature_data *sig_sha256,
struct strbuf *msg,
enum sign_mode mode)
{
struct strbuf tmp_buf = STRBUF_INIT;
struct signature_check signature_check = { 0 };
@@ -2855,20 +2892,34 @@ static void handle_strip_if_invalid(struct strbuf *new_data,
ret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);
if (ret) {
const char *signer = signature_check.signer ?
signature_check.signer : _("unknown");
const char *subject;
int subject_len = find_commit_subject(msg->buf, &subject);
warn_invalid_signature(&signature_check, msg->buf, mode);
if (subject_len > 100)
warning(_("stripping invalid signature for commit '%.100s...'\n"
" allegedly by %s"), subject, signer);
else if (subject_len > 0)
warning(_("stripping invalid signature for commit '%.*s'\n"
" allegedly by %s"), subject_len, subject, signer);
else
warning(_("stripping invalid signature for commit\n"
" allegedly by %s"), signer);
if (mode == SIGN_SIGN_IF_INVALID) {
struct strbuf signature = STRBUF_INIT;
struct strbuf payload = STRBUF_INIT;
/*
* NEEDSWORK: To properly support interoperability mode
* when signing commit signatures, the commit buffer
* must be provided in both the repository and
* compatibility object formats. As currently
* implemented, only the repository object format is
* considered meaning compatibility signatures cannot be
* generated. Thus, attempting to sign commit signatures
* in interoperability mode is currently unsupported.
*/
if (the_repository->compat_hash_algo)
die(_("signing commits in interoperability mode is unsupported"));
strbuf_addstr(&payload, signature_check.payload);
if (sign_buffer(&payload, &signature, signed_commit_keyid,
SIGN_BUFFER_USE_DEFAULT_KEY))
die(_("failed to sign commit object"));
add_header_signature(new_data, &signature, the_hash_algo);
strbuf_release(&signature);
strbuf_release(&payload);
}
finalize_commit_buffer(new_data, NULL, NULL, msg);
} else {
@@ -2931,6 +2982,7 @@ static void parse_new_commit(const char *arg)
/* fallthru */
case SIGN_VERBATIM:
case SIGN_STRIP_IF_INVALID:
case SIGN_SIGN_IF_INVALID:
import_one_signature(&sig_sha1, &sig_sha256, v);
break;
@@ -3015,9 +3067,11 @@ static void parse_new_commit(const char *arg)
"encoding %s\n",
encoding);
if (signed_commit_mode == SIGN_STRIP_IF_INVALID &&
if ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||
signed_commit_mode == SIGN_SIGN_IF_INVALID) &&
(sig_sha1.hash_algo || sig_sha256.hash_algo))
handle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);
handle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,
&msg, signed_commit_mode);
else
finalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);
@@ -3064,6 +3118,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)
case SIGN_STRIP_IF_INVALID:
die(_("'strip-if-invalid' is not a valid mode for "
"git fast-import with --signed-tags=<mode>"));
case SIGN_SIGN_IF_INVALID:
die(_("'sign-if-invalid' is not a valid mode for "
"git fast-import with --signed-tags=<mode>"));
default:
BUG("invalid signed_tag_mode value %d from tag '%s'",
signed_tag_mode, name);
@@ -3653,10 +3710,10 @@ static int parse_one_option(const char *option)
} else if (skip_prefix(option, "export-pack-edges=", &option)) {
option_export_pack_edges(option);
} else if (skip_prefix(option, "signed-commits=", &option)) {
if (parse_sign_mode(option, &signed_commit_mode))
if (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid))
usagef(_("unknown --signed-commits mode '%s'"), option);
} else if (skip_prefix(option, "signed-tags=", &option)) {
if (parse_sign_mode(option, &signed_tag_mode))
if (parse_sign_mode(option, &signed_tag_mode, NULL))
usagef(_("unknown --signed-tags mode '%s'"), option);
} else if (!strcmp(option, "quiet")) {
show_stats = 0;

View File

@@ -167,7 +167,7 @@ static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,
char *keyid = get_signing_key();
int ret = -1;
if (sign_buffer(buffer, &sig, keyid))
if (sign_buffer(buffer, &sig, keyid, 0))
goto out;
if (compat) {
@@ -176,7 +176,7 @@ static int do_sign(struct strbuf *buffer, struct object_id **compat_oid,
if (convert_object_file(the_repository ,&compat_buf, algo, compat,
buffer->buf, buffer->len, OBJ_TAG, 1))
goto out;
if (sign_buffer(&compat_buf, &compat_sig, keyid))
if (sign_buffer(&compat_buf, &compat_sig, keyid, 0))
goto out;
add_header_signature(&compat_buf, &sig, algo);
strbuf_addbuf(&compat_buf, &compat_sig);

View File

@@ -1170,18 +1170,6 @@ int add_header_signature(struct strbuf *buf, struct strbuf *sig, const struct gi
return 0;
}
static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)
{
char *keyid_to_free = NULL;
int ret = 0;
if (!keyid || !*keyid)
keyid = keyid_to_free = get_signing_key();
if (sign_buffer(buf, sig, keyid))
ret = -1;
free(keyid_to_free);
return ret;
}
int parse_signed_commit(const struct commit *commit,
struct strbuf *payload, struct strbuf *signature,
const struct git_hash_algo *algop)
@@ -1759,7 +1747,8 @@ int commit_tree_extended(const char *msg, size_t msg_len,
oidcpy(&parent_buf[i++], &p->item->object.oid);
write_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);
if (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {
if (sign_commit && sign_buffer(&buffer, &sig, sign_commit,
SIGN_BUFFER_USE_DEFAULT_KEY)) {
result = -1;
goto out;
}
@@ -1791,7 +1780,9 @@ int commit_tree_extended(const char *msg, size_t msg_len,
free_commit_extra_headers(compat_extra);
free(mapped_parents);
if (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {
if (sign_commit && sign_buffer(&compat_buffer, &compat_sig,
sign_commit,
SIGN_BUFFER_USE_DEFAULT_KEY)) {
result = -1;
goto out;
}

View File

@@ -400,8 +400,6 @@ LAST_ARG_MUST_BE_NULL
int run_commit_hook(int editor_is_used, const char *index_file,
int *invoked_hook, const char *name, ...);
/* Sign a commit or tag buffer, storing the result in a header. */
int sign_with_header(struct strbuf *buf, const char *keyid);
/* Parse the signature out of a header. */
int parse_buffer_signed_by_header(const char *buffer,
unsigned long size,

View File

@@ -974,11 +974,20 @@ const char *gpg_trust_level_to_str(enum signature_trust_level level)
return sigcheck_gpg_trust_level[level].display_key;
}
int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
int sign_buffer(struct strbuf *buffer, struct strbuf *signature,
const char *signing_key, enum sign_buffer_flags flags)
{
char *keyid_to_free = NULL;
int ret = 0;
gpg_interface_lazy_init();
return use_format->sign_buffer(buffer, signature, 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);
free(keyid_to_free);
return ret;
}
/*
@@ -1143,21 +1152,28 @@ out:
return ret;
}
int parse_sign_mode(const char *arg, enum sign_mode *mode)
int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)
{
if (!strcmp(arg, "abort"))
if (!strcmp(arg, "abort")) {
*mode = SIGN_ABORT;
else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore"))
} else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore")) {
*mode = SIGN_VERBATIM;
else if (!strcmp(arg, "warn-verbatim") || !strcmp(arg, "warn"))
} else if (!strcmp(arg, "warn-verbatim") || !strcmp(arg, "warn")) {
*mode = SIGN_WARN_VERBATIM;
else if (!strcmp(arg, "warn-strip"))
} else if (!strcmp(arg, "warn-strip")) {
*mode = SIGN_WARN_STRIP;
else if (!strcmp(arg, "strip"))
} else if (!strcmp(arg, "strip")) {
*mode = SIGN_STRIP;
else if (!strcmp(arg, "strip-if-invalid"))
} else if (!strcmp(arg, "strip-if-invalid")) {
*mode = SIGN_STRIP_IF_INVALID;
else
} else if (!strcmp(arg, "sign-if-invalid")) {
*mode = SIGN_SIGN_IF_INVALID;
} else if (skip_prefix(arg, "sign-if-invalid=", &arg)) {
*mode = SIGN_SIGN_IF_INVALID;
if (keyid)
*keyid = arg;
} else {
return -1;
}
return 0;
}

View File

@@ -74,6 +74,15 @@ int parse_signature(const char *buf, size_t size, struct strbuf *payload, struct
*/
size_t parse_signed_buffer(const char *buf, size_t size);
/* Flags for sign_buffer(). */
enum sign_buffer_flags {
/*
* Use the default configured signing key as returned by `get_signing_key()`
* when the provided "signing_key" is NULL or empty.
*/
SIGN_BUFFER_USE_DEFAULT_KEY = (1 << 0),
};
/*
* Create a detached signature for the contents of "buffer" and append
* it after "signature"; "buffer" and "signature" can be the same
@@ -81,8 +90,7 @@ size_t parse_signed_buffer(const char *buf, size_t size);
* at the end. Returns 0 on success, non-zero on failure.
*/
int sign_buffer(struct strbuf *buffer, struct strbuf *signature,
const char *signing_key);
const char *signing_key, enum sign_buffer_flags flags);
/*
* Returns corresponding string in lowercase for a given member of
@@ -112,12 +120,15 @@ enum sign_mode {
SIGN_WARN_STRIP,
SIGN_STRIP,
SIGN_STRIP_IF_INVALID,
SIGN_SIGN_IF_INVALID,
};
/*
* Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1
* otherwise.
* otherwise. If the parsed mode is SIGN_SIGN_IF_INVALID and GPG key provided in
* the arguments in the form `sign-if-invalid=<keyid>`, the key-ID is parsed
* into `char **keyid`.
*/
int parse_sign_mode(const char *arg, enum sign_mode *mode);
int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);
#endif

View File

@@ -391,7 +391,7 @@ static int generate_push_cert(struct strbuf *req_buf,
if (!update_seen)
goto free_return;
if (sign_buffer(&cert, &cert, signing_key))
if (sign_buffer(&cert, &cert, signing_key, 0))
die(_("failed to sign the push certificate"));
packet_buf_write(req_buf, "push-cert%c%s", 0, cap_string);

View File

@@ -103,71 +103,111 @@ test_expect_success RUST,GPG 'strip both OpenPGP signatures with --signed-commit
test_line_count = 2 out
'
test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '
git fast-export main >output &&
git -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&
test_must_be_empty log
'
for mode in strip-if-invalid sign-if-invalid
do
test_expect_success GPG "import commit with no signature with --signed-commits=$mode" '
git fast-export main >output &&
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
test_must_be_empty log
'
test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '
test_expect_success GPG "keep valid OpenPGP signature with --signed-commits=$mode" '
rm -rf new &&
git init new &&
git fast-export --signed-commits=verbatim openpgp-signing >output &&
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
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(-sha256)? " actual &&
test_must_be_empty log
'
test_expect_success GPG "handle signature invalidated by message change with --signed-commits=$mode" '
rm -rf new &&
git init new &&
git fast-export --signed-commits=verbatim openpgp-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=$mode <modified >log 2>&1 &&
IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
test $OPENPGP_SIGNING != $IMPORTED &&
git -C new cat-file commit "$IMPORTED" >actual &&
if test "$mode" = strip-if-invalid
then
test_grep "stripping invalid signature" log &&
test_grep ! -E "^gpgsig" actual
else
test_grep "replacing invalid signature" log &&
test_grep -E "^gpgsig(-sha256)? " actual &&
git -C new verify-commit "$IMPORTED"
fi
'
test_expect_success GPGSM "keep valid X.509 signature with --signed-commits=$mode" '
rm -rf new &&
git init new &&
git fast-export --signed-commits=verbatim x509-signing >output &&
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
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_must_be_empty log
'
test_expect_success GPGSSH "keep valid SSH signature with --signed-commits=$mode" '
rm -rf new &&
git init new &&
test_config -C new gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
git fast-export --signed-commits=verbatim ssh-signing >output &&
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
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_must_be_empty log
'
done
test_expect_success GPGSSH "sign invalid commit with explicit keyid" '
rm -rf new &&
git init new &&
git fast-export --signed-commits=verbatim openpgp-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/openpgp-signing) &&
test $OPENPGP_SIGNING = $IMPORTED &&
git -C new cat-file commit "$IMPORTED" >actual &&
test_grep -E "^gpgsig(-sha256)? " actual &&
test_must_be_empty log
'
test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '
rm -rf new &&
git init new &&
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 &&
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
'
test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '
rm -rf new &&
git init new &&
git fast-export --signed-commits=verbatim x509-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/x509-signing) &&
test $X509_SIGNING = $IMPORTED &&
git -C new cat-file commit "$IMPORTED" >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 &&
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) &&
test $SSH_SIGNING = $IMPORTED &&
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"
'
test_done