diff --git a/gpg-interface.c b/gpg-interface.c index db17d65f8a..efe2c0d386 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -75,48 +75,80 @@ void signature_check_clear(struct signature_check *sigc) FREE_AND_NULL(sigc->key); } +/* An exclusive status -- only one of them can appear in output */ +#define GPG_STATUS_EXCLUSIVE (1<<0) + static struct { char result; const char *check; + unsigned int flags; } sigcheck_gpg_status[] = { - { 'G', "\n[GNUPG:] GOODSIG " }, - { 'B', "\n[GNUPG:] BADSIG " }, - { 'U', "\n[GNUPG:] TRUST_NEVER" }, - { 'U', "\n[GNUPG:] TRUST_UNDEFINED" }, - { 'E', "\n[GNUPG:] ERRSIG "}, - { 'X', "\n[GNUPG:] EXPSIG "}, - { 'Y', "\n[GNUPG:] EXPKEYSIG "}, - { 'R', "\n[GNUPG:] REVKEYSIG "}, + { 'G', "GOODSIG ", GPG_STATUS_EXCLUSIVE }, + { 'B', "BADSIG ", GPG_STATUS_EXCLUSIVE }, + { 'U', "TRUST_NEVER", 0 }, + { 'U', "TRUST_UNDEFINED", 0 }, + { 'E', "ERRSIG ", GPG_STATUS_EXCLUSIVE }, + { 'X', "EXPSIG ", GPG_STATUS_EXCLUSIVE }, + { 'Y', "EXPKEYSIG ", GPG_STATUS_EXCLUSIVE }, + { 'R', "REVKEYSIG ", GPG_STATUS_EXCLUSIVE }, }; static void parse_gpg_output(struct signature_check *sigc) { const char *buf = sigc->gpg_status; + const char *line, *next; int i; + int seen_exclusive_status = 0; - /* Iterate over all search strings */ - for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) { - const char *found, *next; + /* Iterate over all lines */ + for (line = buf; *line; line = strchrnul(line+1, '\n')) { + while (*line == '\n') + line++; + /* Skip lines that don't start with GNUPG status */ + if (!skip_prefix(line, "[GNUPG:] ", &line)) + continue; - if (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) { - found = strstr(buf, sigcheck_gpg_status[i].check); - if (!found) - continue; - found += strlen(sigcheck_gpg_status[i].check); - } - sigc->result = sigcheck_gpg_status[i].result; - /* The trust messages are not followed by key/signer information */ - if (sigc->result != 'U') { - next = strchrnul(found, ' '); - sigc->key = xmemdupz(found, next - found); - /* The ERRSIG message is not followed by signer information */ - if (*next && sigc-> result != 'E') { - found = next + 1; - next = strchrnul(found, '\n'); - sigc->signer = xmemdupz(found, next - found); + /* Iterate over all search strings */ + for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) { + if (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) { + if (sigcheck_gpg_status[i].flags & GPG_STATUS_EXCLUSIVE) { + if (++seen_exclusive_status > 1) + goto found_duplicate_status; + } + + sigc->result = sigcheck_gpg_status[i].result; + /* The trust messages are not followed by key/signer information */ + if (sigc->result != 'U') { + next = strchrnul(line, ' '); + free(sigc->key); + sigc->key = xmemdupz(line, next - line); + /* The ERRSIG message is not followed by signer information */ + if (*next && sigc->result != 'E') { + line = next + 1; + next = strchrnul(line, '\n'); + free(sigc->signer); + sigc->signer = xmemdupz(line, next - line); + } + } + + break; } } } + return; + +found_duplicate_status: + /* + * GOODSIG, BADSIG etc. can occur only once for each signature. + * Therefore, if we had more than one then we're dealing with multiple + * signatures. We don't support them currently, and they're rather + * hard to create, so something is likely fishy and we should reject + * them altogether. + */ + sigc->result = 'E'; + /* Clear partial data to avoid confusion */ + FREE_AND_NULL(sigc->signer); + FREE_AND_NULL(sigc->key); } int check_signature(const char *payload, size_t plen, const char *signature, diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh index 4e37ff8f16..180f0be914 100755 --- a/t/t7510-signed-commit.sh +++ b/t/t7510-signed-commit.sh @@ -234,4 +234,30 @@ test_expect_success GPG 'check config gpg.format values' ' test_must_fail git commit -S --amend -m "fail" ' +test_expect_success GPG 'detect fudged commit with double signature' ' + sed -e "/gpgsig/,/END PGP/d" forged1 >double-base && + sed -n -e "/gpgsig/,/END PGP/p" forged1 | \ + sed -e "s/^gpgsig//;s/^ //" | gpg --dearmor >double-sig1.sig && + gpg -o double-sig2.sig -u 29472784 --detach-sign double-base && + cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc && + sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/gpgsig /;2,\$s/^/ /" \ + double-combined.asc > double-gpgsig && + sed -e "/committer/r double-gpgsig" double-base >double-commit && + git hash-object -w -t commit double-commit >double-commit.commit && + test_must_fail git verify-commit $(cat double-commit.commit) && + git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual && + grep "BAD signature from" double-actual && + grep "Good signature from" double-actual +' + +test_expect_success GPG 'show double signature with custom format' ' + cat >expect <<-\EOF && + E + + + EOF + git log -1 --format="%G?%n%GK%n%GS" $(cat double-commit.commit) >actual && + test_cmp expect actual +' + test_done