* `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
~~~~~~~~~~~~~~~~~~~~~
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;
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);
}
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);
}
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 = {
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)
{
- struct strbuf tmp_buf = STRBUF_INIT;
- struct signature_check signature_check = { 0 };
- int ret;
-
- /* Check signature in a temporary commit buffer */
- strbuf_addbuf(&tmp_buf, new_data);
- finalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);
- 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);
+ 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
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 };
+ int ret;
+
+ /* Check signature in a temporary commit buffer */
+ strbuf_addbuf(&tmp_buf, new_data);
+ finalize_commit_buffer(&tmp_buf, sig_sha1, sig_sha256, msg);
+ ret = verify_commit_buffer(tmp_buf.buf, tmp_buf.len, &signature_check);
+
+ if (ret) {
+ warn_invalid_signature(&signature_check, msg->buf, mode);
+
+ 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 {
/* fallthru */
case SIGN_VERBATIM:
case SIGN_STRIP_IF_INVALID:
+ case SIGN_SIGN_IF_INVALID:
import_one_signature(&sig_sha1, &sig_sha256, v);
break;
"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);
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);
} 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;
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;
}
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
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
-'
-
-test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '
- 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' '
+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=$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 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