fast-import: add 'strip-if-invalid' mode to '--signed-tags=<mode>'

With c20f112e51 (fast-import: add 'strip-if-invalid' mode to --signed-commits=<mode>, 2025-11-17), git-fast-import(1) learned to verify commit signatures during import and strip signatures that fail verification. Extend the same behavior to signed tag objects by introducing a 'strip-if-invalid' mode for the '--signed-tags' option. Signed-off-by: Justin Tobler <jltobler@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Justin Tobler committed Mar 26, 2026 at 14:14 UTC 817b0428797742829b57538210ad8404b09f9cb1
3 files changed +110 -10
Documentation/git-fast-import.adoc
+3 -4
@@ -66,11 +66,10 @@ fast-import stream! This option is enabled automatically for
66 remote-helpers that use the `import` capability, as they are
67 already trusted to run their own code.
68
69 -`--signed-tags=(verbatim|warn-verbatim|warn-strip|strip|abort)`::
69 +`--signed-tags=<mode>`::
70 Specify how to handle signed tags. Behaves in the same way as
71 - the `--signed-commits=<mode>` below, except that the
72 - `strip-if-invalid` mode is not yet supported. Like for signed
73 - commits, the default mode is `verbatim`.
71 + the `--signed-commits=<mode>` below. Like for signed commits,
72 + the default mode is `verbatim`.
73
74 `--signed-commits=<mode>`::
75 Specify how to handle signed commits. The following <mode>s
builtin/fast-import.c
+34 -6
@@ -3089,7 +3089,34 @@ static void parse_new_commit(const char *arg)
3089 b->last_commit = object_count_by_type[OBJ_COMMIT];
3090 }
3091
3092 -static void handle_tag_signature(struct strbuf *msg, const char *name)
3092 +static void handle_tag_signature_if_invalid(struct strbuf *buf,
3093 + struct strbuf *msg,
3094 + size_t sig_offset)
3095 +{
3096 + struct strbuf signature = STRBUF_INIT;
3097 + struct strbuf payload = STRBUF_INIT;
3098 + struct signature_check sigc = { 0 };
3099 +
3100 + strbuf_addbuf(&payload, buf);
3101 + strbuf_addch(&payload, '\n');
3102 + strbuf_add(&payload, msg->buf, sig_offset);
3103 + strbuf_add(&signature, msg->buf + sig_offset, msg->len - sig_offset);
3104 +
3105 + sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
3106 + sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
3107 +
3108 + if (!check_signature(&sigc, signature.buf, signature.len))
3109 + goto out;
3110 +
3111 + strbuf_setlen(msg, sig_offset);
3112 +
3113 +out:
3114 + signature_check_clear(&sigc);
3115 + strbuf_release(&signature);
3116 + strbuf_release(&payload);
3117 +}
3118 +
3119 +static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const char *name)
3120 {
3121 size_t sig_offset = parse_signed_buffer(msg->buf, msg->len);
3122
@@ -3115,6 +3142,9 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)
3142 /* Truncate the buffer to remove the signature */
3143 strbuf_setlen(msg, sig_offset);
3144 break;
3145 + case SIGN_STRIP_IF_INVALID:
3146 + handle_tag_signature_if_invalid(buf, msg, sig_offset);
3147 + break;
3148
3149 /* Third, aborting modes */
3150 case SIGN_ABORT:
@@ -3123,9 +3153,6 @@ static void handle_tag_signature(struct strbuf *msg, const char *name)
3153 case SIGN_ABORT_IF_INVALID:
3154 die(_("'abort-if-invalid' is not a valid mode for "
3155 "git fast-import with --signed-tags=<mode>"));
3126 - case SIGN_STRIP_IF_INVALID:
3127 - die(_("'strip-if-invalid' is not a valid mode for "
3128 - "git fast-import with --signed-tags=<mode>"));
3156 case SIGN_SIGN_IF_INVALID:
3157 die(_("'sign-if-invalid' is not a valid mode for "
3158 "git fast-import with --signed-tags=<mode>"));
@@ -3198,8 +3225,6 @@ static void parse_new_tag(const char *arg)
3225 /* tag payload/message */
3226 parse_data(&msg, 0, NULL);
3227
3201 - handle_tag_signature(&msg, t->name);
3202 -
3228 /* build the tag object */
3229 strbuf_reset(&new_data);
3230
@@ -3211,6 +3236,9 @@ static void parse_new_tag(const char *arg)
3236 if (tagger)
3237 strbuf_addf(&new_data,
3238 "tagger %s\n", tagger);
3239 +
3240 + handle_tag_signature(&new_data, &msg, t->name);
3241 +
3242 strbuf_addch(&new_data, '\n');
3243 strbuf_addbuf(&new_data, &msg);
3244 free(tagger);
t/t9306-fast-import-signed-tags.sh
+73
@@ -77,4 +77,77 @@ test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=strip' '
77 test_grep ! "SSH SIGNATURE" out
78 '
79
80 +for mode in strip-if-invalid
81 +do
82 + test_expect_success GPG "import tag with no signature with --signed-tags=$mode" '
83 + test_when_finished rm -rf import &&
84 + git init import &&
85 +
86 + git fast-export --signed-tags=verbatim >output &&
87 + git -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&
88 + test_must_be_empty log
89 + '
90 +
91 + test_expect_success GPG "keep valid OpenPGP signature with --signed-tags=$mode" '
92 + test_when_finished rm -rf import &&
93 + git init import &&
94 +
95 + git fast-export --signed-tags=verbatim openpgp-signed >output &&
96 + git -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&
97 + IMPORTED=$(git -C import rev-parse --verify refs/tags/openpgp-signed) &&
98 + test $OPENPGP_SIGNED = $IMPORTED &&
99 + git -C import cat-file tag "$IMPORTED" >actual &&
100 + test_grep -E "^-----BEGIN PGP SIGNATURE-----" actual &&
101 + test_must_be_empty log
102 + '
103 +
104 + test_expect_success GPG "handle signature invalidated by message change with --signed-tags=$mode" '
105 + test_when_finished rm -rf import &&
106 + git init import &&
107 +
108 + git fast-export --signed-tags=verbatim openpgp-signed >output &&
109 +
110 + # Change the tag message, which invalidates the signature. The tag
111 + # message length should not change though, otherwise the corresponding
112 + # `data <length>` command would have to be changed too.
113 + sed "s/OpenPGP signed tag/OpenPGP forged tag/" output >modified &&
114 +
115 + git -C import fast-import --quiet --signed-tags=$mode <modified >log 2>&1 &&
116 +
117 + IMPORTED=$(git -C import rev-parse --verify refs/tags/openpgp-signed) &&
118 + test $OPENPGP_SIGNED != $IMPORTED &&
119 + git -C import cat-file tag "$IMPORTED" >actual &&
120 + test_grep ! -E "^-----BEGIN PGP SIGNATURE-----" actual &&
121 + test_must_be_empty log
122 + '
123 +
124 + test_expect_success GPGSM "keep valid X.509 signature with --signed-tags=$mode" '
125 + test_when_finished rm -rf import &&
126 + git init import &&
127 +
128 + git fast-export --signed-tags=verbatim x509-signed >output &&
129 + git -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&
130 + IMPORTED=$(git -C import rev-parse --verify refs/tags/x509-signed) &&
131 + test $X509_SIGNED = $IMPORTED &&
132 + git -C import cat-file tag x509-signed >actual &&
133 + test_grep -E "^-----BEGIN SIGNED MESSAGE-----" actual &&
134 + test_must_be_empty log
135 + '
136 +
137 + test_expect_success GPGSSH "keep valid SSH signature with --signed-tags=$mode" '
138 + test_when_finished rm -rf import &&
139 + git init import &&
140 +
141 + test_config -C import gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
142 +
143 + git fast-export --signed-tags=verbatim ssh-signed >output &&
144 + git -C import fast-import --quiet --signed-tags=$mode <output >log 2>&1 &&
145 + IMPORTED=$(git -C import rev-parse --verify refs/tags/ssh-signed) &&
146 + test $SSH_SIGNED = $IMPORTED &&
147 + git -C import cat-file tag ssh-signed >actual &&
148 + test_grep -E "^-----BEGIN SSH SIGNATURE-----" actual &&
149 + test_must_be_empty log
150 + '
151 +done
152 +
153 test_done