Skip to content

Commit bea1a42

Browse files
committed
fast-import: add mode to re-sign invalid commit signatures
With git-fast-import(1), handling of signed commits is controlled via the `--signed-commits=<mode>` option. When an invalid signature is encountered, a user may want the option to re-sign the commit as opposed to just stripping the signature. To facilitate this, introduce a "re-sign-if-invalid" mode for the `--signed-commits` option. Optionally, a key ID may be explicitly provided in the form `re-sign-if-invalid[=<keyid>]` to specify which signing key should be used when re-signing invalid commit signatures. Note that to properly support interoperability mode when re-signing commit signatures, the commit buffer must be created in both the repository and compatability object formats to generate the appropriate signatures accordingly. As currently implemented, the commit buffer for the compatability object format is not reconstructed and thus re-signing commits in interoperability mode is not yet supported. Support may be added in the future. Signed-off-by: Justin Tobler <jltobler@gmail.com>
1 parent 4990255 commit bea1a42

6 files changed

Lines changed: 183 additions & 78 deletions

File tree

Documentation/git-fast-import.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ already trusted to run their own code.
8686
* `strip-if-invalid` will check signatures and, if they are invalid,
8787
will strip them and display a warning. The validation is performed
8888
in the same way as linkgit:git-verify-commit[1] does it.
89+
* `re-sign-if-invalid[=<keyid>]`, similar to `strip-if-invalid`, verifies
90+
commit signatures and replaces invalid signatures with newly created ones.
91+
Valid signatures are left unchanged. If `<keyid>` is provided, that key is
92+
used for re-signing; otherwise the configured default signing key is used.
8993

9094
Options for Frontends
9195
~~~~~~~~~~~~~~~~~~~~~

builtin/fast-export.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ static int parse_opt_sign_mode(const struct option *opt,
6464
if (unset)
6565
return 0;
6666

67-
if (parse_sign_mode(arg, val))
67+
if (parse_sign_mode(arg, val, NULL))
6868
return error(_("unknown %s mode: %s"), opt->long_name, arg);
6969

7070
return 0;
@@ -825,6 +825,9 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
825825
case SIGN_STRIP_IF_INVALID:
826826
die(_("'strip-if-invalid' is not a valid mode for "
827827
"git fast-export with --signed-commits=<mode>"));
828+
case SIGN_RESIGN_IF_INVALID:
829+
die(_("'re-sign-if-invalid' is not a valid mode for "
830+
"git fast-export with --signed-commits=<mode>"));
828831
default:
829832
BUG("invalid signed_commit_mode value %d", signed_commit_mode);
830833
}
@@ -970,6 +973,9 @@ static void handle_tag(const char *name, struct tag *tag)
970973
case SIGN_STRIP_IF_INVALID:
971974
die(_("'strip-if-invalid' is not a valid mode for "
972975
"git fast-export with --signed-tags=<mode>"));
976+
case SIGN_RESIGN_IF_INVALID:
977+
die(_("'re-sign-if-invalid' is not a valid mode for "
978+
"git fast-export with --signed-tags=<mode>"));
973979
default:
974980
BUG("invalid signed_commit_mode value %d", signed_commit_mode);
975981
}

builtin/fast-import.c

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ static const char *global_prefix;
190190

191191
static enum sign_mode signed_tag_mode = SIGN_VERBATIM;
192192
static enum sign_mode signed_commit_mode = SIGN_VERBATIM;
193+
static const char *signed_commit_keyid;
193194

194195
/* Memory pools */
195196
static struct mem_pool fi_mem_pool = {
@@ -2836,10 +2837,11 @@ static void finalize_commit_buffer(struct strbuf *new_data,
28362837
strbuf_addbuf(new_data, msg);
28372838
}
28382839

2839-
static void handle_strip_if_invalid(struct strbuf *new_data,
2840-
struct signature_data *sig_sha1,
2841-
struct signature_data *sig_sha256,
2842-
struct strbuf *msg)
2840+
static void handle_signature_if_invalid(struct strbuf *new_data,
2841+
struct signature_data *sig_sha1,
2842+
struct signature_data *sig_sha256,
2843+
struct strbuf *msg,
2844+
enum sign_mode mode)
28432845
{
28442846
struct strbuf tmp_buf = STRBUF_INIT;
28452847
struct signature_check signature_check = { 0 };
@@ -2856,15 +2858,52 @@ static void handle_strip_if_invalid(struct strbuf *new_data,
28562858
const char *subject;
28572859
int subject_len = find_commit_subject(msg->buf, &subject);
28582860

2859-
if (subject_len > 100)
2860-
warning(_("stripping invalid signature for commit '%.100s...'\n"
2861-
" allegedly by %s"), subject, signer);
2862-
else if (subject_len > 0)
2863-
warning(_("stripping invalid signature for commit '%.*s'\n"
2864-
" allegedly by %s"), subject_len, subject, signer);
2865-
else
2866-
warning(_("stripping invalid signature for commit\n"
2867-
" allegedly by %s"), signer);
2861+
if (mode == SIGN_STRIP_IF_INVALID) {
2862+
if (subject_len > 100)
2863+
warning(_("stripping invalid signature for commit '%.100s...'\n"
2864+
" allegedly by %s"), subject, signer);
2865+
else if (subject_len > 0)
2866+
warning(_("stripping invalid signature for commit '%.*s'\n"
2867+
" allegedly by %s"), subject_len, subject, signer);
2868+
else
2869+
warning(_("stripping invalid signature for commit\n"
2870+
" allegedly by %s"), signer);
2871+
} else if (mode == SIGN_RESIGN_IF_INVALID) {
2872+
struct strbuf signature = STRBUF_INIT;
2873+
struct strbuf payload = STRBUF_INIT;
2874+
2875+
if (subject_len > 100)
2876+
warning(_("re-signing invalid signature for commit '%.100s...'\n"
2877+
" allegedly by %s"), subject, signer);
2878+
else if (subject_len > 0)
2879+
warning(_("re-signing invalid signature for commit '%.*s'\n"
2880+
" allegedly by %s"), subject_len, subject, signer);
2881+
else
2882+
warning(_("re-signing invalid signature for commit\n"
2883+
" allegedly by %s"), signer);
2884+
2885+
/*
2886+
* NEEDSWORK: To properly support interoperability mode
2887+
* when re-signing commit signatures, the commit buffer
2888+
* must be provided in both the repository and
2889+
* compatability object formats. As currently
2890+
* implemented, only the repository object format is
2891+
* considered meaning compatability signatures cannot be
2892+
* generated. Thus, attempting to re-sign commit
2893+
* signatures in interoperability mode is currently
2894+
* unsupported.
2895+
*/
2896+
if (the_repository->compat_hash_algo)
2897+
die(_("re-signing signatures in interoperability mode is unsupported"));
2898+
2899+
strbuf_addstr(&payload, signature_check.payload);
2900+
if (sign_buffer_with_key(&payload, &signature, signed_commit_keyid))
2901+
die(_("failed to sign commit object"));
2902+
add_header_signature(new_data, &signature, the_hash_algo);
2903+
2904+
strbuf_release(&signature);
2905+
strbuf_release(&payload);
2906+
}
28682907

28692908
finalize_commit_buffer(new_data, NULL, NULL, msg);
28702909
} else {
@@ -2927,6 +2966,7 @@ static void parse_new_commit(const char *arg)
29272966
/* fallthru */
29282967
case SIGN_VERBATIM:
29292968
case SIGN_STRIP_IF_INVALID:
2969+
case SIGN_RESIGN_IF_INVALID:
29302970
import_one_signature(&sig_sha1, &sig_sha256, v);
29312971
break;
29322972

@@ -3011,9 +3051,11 @@ static void parse_new_commit(const char *arg)
30113051
"encoding %s\n",
30123052
encoding);
30133053

3014-
if (signed_commit_mode == SIGN_STRIP_IF_INVALID &&
3054+
if ((signed_commit_mode == SIGN_STRIP_IF_INVALID ||
3055+
signed_commit_mode == SIGN_RESIGN_IF_INVALID) &&
30153056
(sig_sha1.hash_algo || sig_sha256.hash_algo))
3016-
handle_strip_if_invalid(&new_data, &sig_sha1, &sig_sha256, &msg);
3057+
handle_signature_if_invalid(&new_data, &sig_sha1, &sig_sha256,
3058+
&msg, signed_commit_mode);
30173059
else
30183060
finalize_commit_buffer(&new_data, &sig_sha1, &sig_sha256, &msg);
30193061

@@ -3060,6 +3102,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)
30603102
case SIGN_STRIP_IF_INVALID:
30613103
die(_("'strip-if-invalid' is not a valid mode for "
30623104
"git fast-import with --signed-tags=<mode>"));
3105+
case SIGN_RESIGN_IF_INVALID:
3106+
die(_("'re-sign-if-invalid' is not a valid mode for "
3107+
"git fast-import with --signed-tags=<mode>"));
30633108
default:
30643109
BUG("invalid signed_tag_mode value %d from tag '%s'",
30653110
signed_tag_mode, name);
@@ -3649,10 +3694,10 @@ static int parse_one_option(const char *option)
36493694
} else if (skip_prefix(option, "export-pack-edges=", &option)) {
36503695
option_export_pack_edges(option);
36513696
} else if (skip_prefix(option, "signed-commits=", &option)) {
3652-
if (parse_sign_mode(option, &signed_commit_mode))
3697+
if (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid))
36533698
usagef(_("unknown --signed-commits mode '%s'"), option);
36543699
} else if (skip_prefix(option, "signed-tags=", &option)) {
3655-
if (parse_sign_mode(option, &signed_tag_mode))
3700+
if (parse_sign_mode(option, &signed_tag_mode, NULL))
36563701
usagef(_("unknown --signed-tags mode '%s'"), option);
36573702
} else if (!strcmp(option, "quiet")) {
36583703
show_stats = 0;

gpg-interface.c

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,21 +1155,28 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
11551155
return ret;
11561156
}
11571157

1158-
int parse_sign_mode(const char *arg, enum sign_mode *mode)
1158+
int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid)
11591159
{
1160-
if (!strcmp(arg, "abort"))
1160+
if (!strcmp(arg, "abort")) {
11611161
*mode = SIGN_ABORT;
1162-
else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore"))
1162+
} else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore")) {
11631163
*mode = SIGN_VERBATIM;
1164-
else if (!strcmp(arg, "warn-verbatim") || !strcmp(arg, "warn"))
1164+
} else if (!strcmp(arg, "warn-verbatim") || !strcmp(arg, "warn")) {
11651165
*mode = SIGN_WARN_VERBATIM;
1166-
else if (!strcmp(arg, "warn-strip"))
1166+
} else if (!strcmp(arg, "warn-strip")) {
11671167
*mode = SIGN_WARN_STRIP;
1168-
else if (!strcmp(arg, "strip"))
1168+
} else if (!strcmp(arg, "strip")) {
11691169
*mode = SIGN_STRIP;
1170-
else if (!strcmp(arg, "strip-if-invalid"))
1170+
} else if (!strcmp(arg, "strip-if-invalid")) {
11711171
*mode = SIGN_STRIP_IF_INVALID;
1172-
else
1172+
} else if (!strcmp(arg, "re-sign-if-invalid")) {
1173+
*mode = SIGN_RESIGN_IF_INVALID;
1174+
} else if (skip_prefix(arg, "re-sign-if-invalid=", &arg)) {
1175+
*mode = SIGN_RESIGN_IF_INVALID;
1176+
if (keyid)
1177+
*keyid = arg;
1178+
} else {
11731179
return -1;
1180+
}
11741181
return 0;
11751182
}

gpg-interface.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,15 @@ enum sign_mode {
119119
SIGN_WARN_STRIP,
120120
SIGN_STRIP,
121121
SIGN_STRIP_IF_INVALID,
122+
SIGN_RESIGN_IF_INVALID,
122123
};
123124

124125
/*
125126
* Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1
126-
* otherwise.
127+
* otherwise. If the parsed mode is SIGN_RESIGN_IF_INVALID and GPG key provided
128+
* in the arguments in the form `re-sign-if-invalid=<keyid>`, the key-ID is
129+
* parsed into `char **keyid`.
127130
*/
128-
int parse_sign_mode(const char *arg, enum sign_mode *mode);
131+
int parse_sign_mode(const char *arg, enum sign_mode *mode, const char **keyid);
129132

130133
#endif

t/t9305-fast-import-signatures.sh

Lines changed: 90 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -103,26 +103,85 @@ test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=war
103103
test_line_count = 2 out
104104
'
105105

106-
test_expect_success GPG 'import commit with no signature with --signed-commits=strip-if-invalid' '
107-
git fast-export main >output &&
108-
git -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&
109-
test_must_be_empty log
110-
'
111-
112-
test_expect_success GPG 'keep valid OpenPGP signature with --signed-commits=strip-if-invalid' '
113-
rm -rf new &&
114-
git init new &&
115-
116-
git fast-export --signed-commits=verbatim openpgp-signing >output &&
117-
git -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&
118-
IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
119-
test $OPENPGP_SIGNING = $IMPORTED &&
120-
git -C new cat-file commit "$IMPORTED" >actual &&
121-
test_grep -E "^gpgsig(-sha256)? " actual &&
122-
test_must_be_empty log
123-
'
124-
125-
test_expect_success GPG 'strip signature invalidated by message change with --signed-commits=strip-if-invalid' '
106+
for mode in strip-if-invalid re-sign-if-invalid
107+
do
108+
test_expect_success GPG "import commit with no signature with --signed-commits=$mode" '
109+
git fast-export main >output &&
110+
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
111+
test_must_be_empty log
112+
'
113+
114+
test_expect_success GPG "keep valid OpenPGP signature with --signed-commits=$mode" '
115+
rm -rf new &&
116+
git init new &&
117+
118+
git fast-export --signed-commits=verbatim openpgp-signing >output &&
119+
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
120+
IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
121+
test $OPENPGP_SIGNING = $IMPORTED &&
122+
git -C new cat-file commit "$IMPORTED" >actual &&
123+
test_grep -E "^gpgsig(-sha256)? " actual &&
124+
test_must_be_empty log
125+
'
126+
127+
test_expect_success GPG "handle signature invalidated by message change with --signed-commits=$mode" '
128+
rm -rf new &&
129+
git init new &&
130+
131+
git fast-export --signed-commits=verbatim openpgp-signing >output &&
132+
133+
# Change the commit message, which invalidates the signature.
134+
# The commit message length should not change though, otherwise the
135+
# corresponding `data <length>` command would have to be changed too.
136+
sed "s/OpenPGP signed commit/OpenPGP forged commit/" output >modified &&
137+
138+
git -C new fast-import --quiet --signed-commits=$mode <modified >log 2>&1 &&
139+
140+
IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
141+
test $OPENPGP_SIGNING != $IMPORTED &&
142+
git -C new cat-file commit "$IMPORTED" >actual &&
143+
144+
if test "$mode" = strip-if-invalid
145+
then
146+
test_grep "stripping invalid signature" log &&
147+
test_grep ! -E "^gpgsig" actual
148+
else
149+
test_grep "re-signing invalid signature" log &&
150+
test_grep -E "^gpgsig(-sha256)? " actual &&
151+
git -C new verify-commit "$IMPORTED"
152+
fi
153+
'
154+
155+
test_expect_success GPGSM "keep valid X.509 signature with --signed-commits=$mode" '
156+
rm -rf new &&
157+
git init new &&
158+
159+
git fast-export --signed-commits=verbatim x509-signing >output &&
160+
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
161+
IMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&
162+
test $X509_SIGNING = $IMPORTED &&
163+
git -C new cat-file commit "$IMPORTED" >actual &&
164+
test_grep -E "^gpgsig(-sha256)? " actual &&
165+
test_must_be_empty log
166+
'
167+
168+
test_expect_success GPGSSH "keep valid SSH signature with --signed-commits=$mode" '
169+
rm -rf new &&
170+
git init new &&
171+
172+
test_config -C new gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
173+
174+
git fast-export --signed-commits=verbatim ssh-signing >output &&
175+
git -C new fast-import --quiet --signed-commits=$mode <output >log 2>&1 &&
176+
IMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&
177+
test $SSH_SIGNING = $IMPORTED &&
178+
git -C new cat-file commit "$IMPORTED" >actual &&
179+
test_grep -E "^gpgsig(-sha256)? " actual &&
180+
test_must_be_empty log
181+
'
182+
done
183+
184+
test_expect_success GPGSSH "re-sign invalid commit with explicit keyid" '
126185
rm -rf new &&
127186
git init new &&
128187
@@ -133,41 +192,22 @@ test_expect_success GPG 'strip signature invalidated by message change with --si
133192
# corresponding `data <length>` command would have to be changed too.
134193
sed "s/OpenPGP signed commit/OpenPGP forged commit/" output >modified &&
135194
136-
git -C new fast-import --quiet --signed-commits=strip-if-invalid <modified >log 2>&1 &&
195+
# Configure the target repository with an invalid default signing key.
196+
test_config -C new user.signingkey "not-a-real-key-id" &&
197+
test_config -C new gpg.format ssh &&
198+
test_config -C new gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
199+
test_must_fail git -C new fast-import --quiet \
200+
--signed-commits=re-sign-if-invalid <modified >/dev/null 2>&1 &&
201+
202+
# Import using explicitly provided signing key.
203+
git -C new fast-import --quiet \
204+
--signed-commits=re-sign-if-invalid="${GPGSSH_KEY_PRIMARY}" <modified &&
137205
138206
IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
139207
test $OPENPGP_SIGNING != $IMPORTED &&
140208
git -C new cat-file commit "$IMPORTED" >actual &&
141-
test_grep ! -E "^gpgsig" actual &&
142-
test_grep "stripping invalid signature" log
143-
'
144-
145-
test_expect_success GPGSM 'keep valid X.509 signature with --signed-commits=strip-if-invalid' '
146-
rm -rf new &&
147-
git init new &&
148-
149-
git fast-export --signed-commits=verbatim x509-signing >output &&
150-
git -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&
151-
IMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&
152-
test $X509_SIGNING = $IMPORTED &&
153-
git -C new cat-file commit "$IMPORTED" >actual &&
154209
test_grep -E "^gpgsig(-sha256)? " actual &&
155-
test_must_be_empty log
156-
'
157-
158-
test_expect_success GPGSSH 'keep valid SSH signature with --signed-commits=strip-if-invalid' '
159-
rm -rf new &&
160-
git init new &&
161-
162-
test_config -C new gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
163-
164-
git fast-export --signed-commits=verbatim ssh-signing >output &&
165-
git -C new fast-import --quiet --signed-commits=strip-if-invalid <output >log 2>&1 &&
166-
IMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&
167-
test $SSH_SIGNING = $IMPORTED &&
168-
git -C new cat-file commit "$IMPORTED" >actual &&
169-
test_grep -E "^gpgsig(-sha256)? " actual &&
170-
test_must_be_empty log
210+
git -C new verify-commit "$IMPORTED"
171211
'
172212

173213
test_done

0 commit comments

Comments
 (0)