Skip to content

Commit da6cf1b

Browse files
mgornygitster
authored andcommitted
gpg-interface.c: detect and reject multiple signatures on commits
GnuPG supports creating signatures consisting of multiple signature packets. If such a signature is verified, it outputs all the status messages for each signature separately. However, git currently does not account for such scenario and gets terribly confused over getting multiple *SIG statuses. For example, if a malicious party alters a signed commit and appends a new untrusted signature, git is going to ignore the original bad signature and report untrusted commit instead. However, %GK and %GS format strings may still expand to the data corresponding to the original signature, potentially tricking the scripts into trusting the malicious commit. Given that the use of multiple signatures is quite rare, git does not support creating them without jumping through a few hoops, and finally supporting them properly would require extensive API improvement, it seems reasonable to just reject them at the moment. Signed-off-by: Michał Górny <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent cae598d commit da6cf1b

File tree

2 files changed

+87
-29
lines changed

2 files changed

+87
-29
lines changed

gpg-interface.c

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -75,48 +75,80 @@ void signature_check_clear(struct signature_check *sigc)
7575
FREE_AND_NULL(sigc->key);
7676
}
7777

78+
/* An exclusive status -- only one of them can appear in output */
79+
#define GPG_STATUS_EXCLUSIVE (1<<0)
80+
7881
static struct {
7982
char result;
8083
const char *check;
84+
unsigned int flags;
8185
} sigcheck_gpg_status[] = {
82-
{ 'G', "\n[GNUPG:] GOODSIG " },
83-
{ 'B', "\n[GNUPG:] BADSIG " },
84-
{ 'U', "\n[GNUPG:] TRUST_NEVER" },
85-
{ 'U', "\n[GNUPG:] TRUST_UNDEFINED" },
86-
{ 'E', "\n[GNUPG:] ERRSIG "},
87-
{ 'X', "\n[GNUPG:] EXPSIG "},
88-
{ 'Y', "\n[GNUPG:] EXPKEYSIG "},
89-
{ 'R', "\n[GNUPG:] REVKEYSIG "},
86+
{ 'G', "GOODSIG ", GPG_STATUS_EXCLUSIVE },
87+
{ 'B', "BADSIG ", GPG_STATUS_EXCLUSIVE },
88+
{ 'U', "TRUST_NEVER", 0 },
89+
{ 'U', "TRUST_UNDEFINED", 0 },
90+
{ 'E', "ERRSIG ", GPG_STATUS_EXCLUSIVE },
91+
{ 'X', "EXPSIG ", GPG_STATUS_EXCLUSIVE },
92+
{ 'Y', "EXPKEYSIG ", GPG_STATUS_EXCLUSIVE },
93+
{ 'R', "REVKEYSIG ", GPG_STATUS_EXCLUSIVE },
9094
};
9195

9296
static void parse_gpg_output(struct signature_check *sigc)
9397
{
9498
const char *buf = sigc->gpg_status;
99+
const char *line, *next;
95100
int i;
96-
97-
/* Iterate over all search strings */
98-
for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {
99-
const char *found, *next;
100-
101-
if (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) {
102-
found = strstr(buf, sigcheck_gpg_status[i].check);
103-
if (!found)
104-
continue;
105-
found += strlen(sigcheck_gpg_status[i].check);
106-
}
107-
sigc->result = sigcheck_gpg_status[i].result;
108-
/* The trust messages are not followed by key/signer information */
109-
if (sigc->result != 'U') {
110-
next = strchrnul(found, ' ');
111-
sigc->key = xmemdupz(found, next - found);
112-
/* The ERRSIG message is not followed by signer information */
113-
if (*next && sigc-> result != 'E') {
114-
found = next + 1;
115-
next = strchrnul(found, '\n');
116-
sigc->signer = xmemdupz(found, next - found);
101+
int seen_exclusive_status = 0;
102+
103+
/* Iterate over all lines */
104+
for (line = buf; *line; line = strchrnul(line+1, '\n')) {
105+
while (*line == '\n')
106+
line++;
107+
/* Skip lines that don't start with GNUPG status */
108+
if (!skip_prefix(line, "[GNUPG:] ", &line))
109+
continue;
110+
111+
/* Iterate over all search strings */
112+
for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {
113+
if (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {
114+
if (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) {
115+
if (++seen_exclusive_status > 1)
116+
goto found_duplicate_status;
117+
}
118+
119+
sigc->result = sigcheck_gpg_status[i].result;
120+
/* The trust messages are not followed by key/signer information */
121+
if (sigc->result != 'U') {
122+
next = strchrnul(line, ' ');
123+
free(sigc->key);
124+
sigc->key = xmemdupz(line, next - line);
125+
/* The ERRSIG message is not followed by signer information */
126+
if (*next && sigc->result != 'E') {
127+
line = next + 1;
128+
next = strchrnul(line, '\n');
129+
free(sigc->signer);
130+
sigc->signer = xmemdupz(line, next - line);
131+
}
132+
}
133+
134+
break;
117135
}
118136
}
119137
}
138+
return;
139+
140+
found_duplicate_status:
141+
/*
142+
* GOODSIG, BADSIG etc. can occur only once for each signature.
143+
* Therefore, if we had more than one then we're dealing with multiple
144+
* signatures. We don't support them currently, and they're rather
145+
* hard to create, so something is likely fishy and we should reject
146+
* them altogether.
147+
*/
148+
sigc->result = 'E';
149+
/* Clear partial data to avoid confusion */
150+
FREE_AND_NULL(sigc->signer);
151+
FREE_AND_NULL(sigc->key);
120152
}
121153

122154
int check_signature(const char *payload, size_t plen, const char *signature,

t/t7510-signed-commit.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,30 @@ test_expect_success GPG 'check config gpg.format values' '
234234
test_must_fail git commit -S --amend -m "fail"
235235
'
236236

237+
test_expect_success GPG 'detect fudged commit with double signature' '
238+
sed -e "/gpgsig/,/END PGP/d" forged1 >double-base &&
239+
sed -n -e "/gpgsig/,/END PGP/p" forged1 | \
240+
sed -e "s/^gpgsig//;s/^ //" | gpg --dearmor >double-sig1.sig &&
241+
gpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&
242+
cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&
243+
sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/gpgsig /;2,\$s/^/ /" \
244+
double-combined.asc > double-gpgsig &&
245+
sed -e "/committer/r double-gpgsig" double-base >double-commit &&
246+
git hash-object -w -t commit double-commit >double-commit.commit &&
247+
test_must_fail git verify-commit $(cat double-commit.commit) &&
248+
git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&
249+
grep "BAD signature from" double-actual &&
250+
grep "Good signature from" double-actual
251+
'
252+
253+
test_expect_success GPG 'show double signature with custom format' '
254+
cat >expect <<-\EOF &&
255+
E
256+
257+
258+
EOF
259+
git log -1 --format="%G?%n%GK%n%GS" $(cat double-commit.commit) >actual &&
260+
test_cmp expect actual
261+
'
262+
237263
test_done

0 commit comments

Comments
 (0)