diff --git a/Documentation/git-verify-commit.adoc b/Documentation/git-verify-commit.adoc index aee4c40eac4666ebacb4cc2fd67e99b8d7e2fbfb..fb038ae0cf271c080713c440fd5ff84281add5a3 100644 --- a/Documentation/git-verify-commit.adoc +++ b/Documentation/git-verify-commit.adoc @@ -3,29 +3,70 @@ git-verify-commit(1) NAME ---- -git-verify-commit - Check the GPG signature of commits +git-verify-commit - Check the signature of commits SYNOPSIS -------- [verse] -'git verify-commit' [-v | --verbose] [--raw] ... +'git verify-commit' [-v | --verbose] [--raw] [--summary] ... DESCRIPTION ----------- -Validates the GPG signature created by 'git commit -S'. +Validates the cryptographic signature of commits. This is typically +a GPG signature created by 'git commit -S', but other signature +formats like SSH may also be verified depending on Git configuration +(see linkgit:git-config[1] and the `gpg.format` option). + +By default, the command prints human-readable verification results to +standard error. + +EXIT STATUS +----------- +If all the specified commits are successfully verified and their +signatures are good and trusted according to the configured trust +requirements, the command exits with 0. + +If any commit fails verification (e.g., due to a bad signature, a +missing or untrusted key), if a specified object cannot be found or is +not a commit object, or if another error occurs during verification, +the command exits with a non-zero status. OPTIONS ------- --raw:: - Print the raw gpg status output to standard error instead of the normal - human-readable output. + Print the raw signature verification status output to standard + error instead of the normal human-readable output. The format + of this output is specific to the signature format being used. + +--summary:: + Print a one-line human-readable summary of the signature check + to standard output in the format: `STATUS FORMAT ALGORITHM`. ++ +STATUS is the result character (e.g., G, B, E, U, N, ...) shown by the +"%G?" pretty format specifier. See the "Pretty Formats" section in +linkgit:git-log[1]. ++ +FORMAT indicates the signature format (`openpgp`, `x509`, or `ssh`) or +`?` if unknown. ++ +ALGORITHM is the hash algorithm used for GPG/GPGSM signatures +(e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures +(`RSA`, `ECDSA`, `ED25519`, ...), or `?` if unknown. -v:: --verbose:: Print the contents of the commit object before validating it. ...:: - SHA-1 identifiers of Git commit objects. + Commit objects to verify. Can be specified using any format + accepted by linkgit:git-rev-parse[1]. + +SEE ALSO +-------- +linkgit:git-commit[1], +linkgit:git-config[1], +linkgit:git-verify-tag[1], +linkgit:git-log[1] GIT --- diff --git a/builtin/verify-commit.c b/builtin/verify-commit.c index 5f749a30daf0155514f54de628a217b09ea0cf91..54b5b7d360bf830b350e694f210a781883044763 100644 --- a/builtin/verify-commit.c +++ b/builtin/verify-commit.c @@ -14,7 +14,7 @@ #include "gpg-interface.h" static const char * const verify_commit_usage[] = { - N_("git verify-commit [-v | --verbose] [--raw] ..."), + N_("git verify-commit [-v | --verbose] [--raw] [--summary] ..."), NULL }; @@ -27,6 +27,7 @@ static int run_gpg_verify(struct commit *commit, unsigned flags) ret = check_commit_signature(commit, &signature_check); print_signature_buffer(&signature_check, flags); + print_signature_summary(&signature_check, flags); signature_check_clear(&signature_check); return ret; @@ -60,6 +61,7 @@ int cmd_verify_commit(int argc, const struct option verify_commit_options[] = { OPT__VERBOSE(&verbose, N_("print commit contents")), OPT_BIT(0, "raw", &flags, N_("print raw gpg status output"), GPG_VERIFY_RAW), + OPT_BIT(0, "summary", &flags, N_("print concise signature verification summary"), GPG_VERIFY_SUMMARY), OPT_END() }; diff --git a/gpg-interface.c b/gpg-interface.c index 0896458de5a9889bf5951d9703c37a67e20d3e1a..fc198715c4c8686de32109c978e4277370557e62 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -153,6 +153,8 @@ void signature_check_clear(struct signature_check *sigc) FREE_AND_NULL(sigc->key); FREE_AND_NULL(sigc->fingerprint); FREE_AND_NULL(sigc->primary_key_fingerprint); + FREE_AND_NULL(sigc->format_name); + FREE_AND_NULL(sigc->sig_algo); } /* An exclusive status -- only one of them can appear in output */ @@ -221,6 +223,65 @@ static int parse_gpg_trust_level(const char *level, return 1; } +/* See RFC 4880: OpenPGP Message Format, section 9.4. Hash Algorithms */ +static struct sigcheck_gpg_hash_algo { + const char *id; + const char *name; +} sigcheck_gpg_hash_algo[] = { + { "1", "md5" }, /* deprecated */ + { "2", "sha1" }, /* mandatory */ + { "3", "ripemd160" }, + { "8", "sha256" }, + { "9", "sha384" }, + { "10", "sha512" }, + { "11", "sha224" }, +}; + +static const char *lookup_gpg_hash_algo(const char *algo_id) +{ + if (!algo_id) + return NULL; + + for (size_t i = 0; i < ARRAY_SIZE(sigcheck_gpg_hash_algo); i++) { + if (!strcmp(sigcheck_gpg_hash_algo[i].id, algo_id)) + return sigcheck_gpg_hash_algo[i].name; + } + + return NULL; +} + +static char *extract_gpg_hash_algo(const char *args_start, + const char *line_end, + int field_index) +{ + const char *p = args_start; + int current_field = 0; + char *result = NULL; + + while (p < line_end && current_field < field_index) { + /* Skip to the end of the current field */ + while (p < line_end && *p != ' ') + p++; + /* Skip spaces to get to the start of the next field */ + while (p < line_end && *p == ' ') { + p++; + current_field++; + } + } + + if (p < line_end && current_field == field_index) { + /* Found start of the target field */ + const char *algo_id_end = strchrnul(p, ' '); + char *algo_id = xmemdupz(p, algo_id_end - p); + const char *hash_algo = lookup_gpg_hash_algo(algo_id); + if (hash_algo) + result = xstrdup(hash_algo); + free(algo_id); + } + + return result; +} + static void parse_gpg_output(struct signature_check *sigc) { const char *buf = sigc->gpg_status; @@ -242,6 +303,18 @@ static void parse_gpg_output(struct signature_check *sigc) /* Iterate over all search strings */ for (size_t i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) { if (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) { + + /* Do we have hash algorithm? */ + if (!sigc->sig_algo) { + const char *line_end = strchrnul(line, '\n'); + if (!strcmp(sigcheck_gpg_status[i].check, "VALIDSIG ")) + /* Hash algorithm is the 8th field in VALIDSIG */ + sigc->sig_algo = extract_gpg_hash_algo(line, line_end, 7); + else if (!strcmp(sigcheck_gpg_status[i].check, "ERRSIG ")) + /* Hash algorithm is the 3rd field in ERRSIG */ + sigc->sig_algo = extract_gpg_hash_algo(line, line_end, 2); + } + /* * GOODSIG, BADSIG etc. can occur only once for * each signature. Therefore, if we had more @@ -323,6 +396,7 @@ static void parse_gpg_output(struct signature_check *sigc) } } } + return; error: @@ -332,6 +406,7 @@ static void parse_gpg_output(struct signature_check *sigc) FREE_AND_NULL(sigc->fingerprint); FREE_AND_NULL(sigc->signer); FREE_AND_NULL(sigc->key); + FREE_AND_NULL(sigc->sig_algo); } static int verify_gpg_signed_buffer(struct signature_check *sigc, @@ -382,11 +457,27 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc, return ret; } +static char *extract_ssh_key_type(const char *type_start, const char *type_end) +{ + if (!type_end || type_end <= type_start) + return NULL; + + /* Back up over any spaces before " key " */ + while (type_end > type_start && *(type_end - 1) == ' ') + type_end--; + + if (type_end <= type_start) + return NULL; + + return xmemdupz(type_start, type_end - type_start); +} + static void parse_ssh_output(struct signature_check *sigc) { const char *line, *principal, *search; char *to_free; char *key = NULL; + const char *after_last_with = NULL; /* * ssh-keygen output should be: @@ -411,8 +502,10 @@ static void parse_ssh_output(struct signature_check *sigc) principal = line; do { search = strstr(line, " with "); - if (search) + if (search) { line = search + 1; + after_last_with = search + 6; + } } while (search != NULL); if (line == principal) goto cleanup; @@ -425,14 +518,18 @@ static void parse_ssh_output(struct signature_check *sigc) /* Valid signature, but key unknown */ sigc->result = 'G'; sigc->trust_level = TRUST_UNDEFINED; + after_last_with = line; } else { goto cleanup; } key = strstr(line, "key "); if (key) { - sigc->fingerprint = xstrdup(strstr(line, "key ") + 4); + sigc->fingerprint = xstrdup(key + 4); sigc->key = xstrdup(sigc->fingerprint); + + if (after_last_with) + sigc->sig_algo = extract_ssh_key_type(after_last_with, key); } else { /* * Output did not match what we expected @@ -660,6 +757,8 @@ int check_signature(struct signature_check *sigc, if (!fmt) die(_("bad/incompatible signature '%s'"), signature); + sigc->format_name = xstrdup(fmt->name); + if (parse_payload_metadata(sigc)) return 1; @@ -686,6 +785,14 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags) fputs(output, stderr); } +void print_signature_summary(const struct signature_check *sigc, unsigned flags) +{ + if (flags & GPG_VERIFY_SUMMARY) + printf("%c %s %s\n", sigc->result, + sigc->format_name ? sigc->format_name : "?", + sigc->sig_algo ? sigc->sig_algo : "?"); +} + size_t parse_signed_buffer(const char *buf, size_t size) { size_t len = 0; diff --git a/gpg-interface.h b/gpg-interface.h index e09f12e8d04d925faa8613d340c50fea71ca0239..a9565757f64819f37f3ec6ab00d493cccc9d4517 100644 --- a/gpg-interface.h +++ b/gpg-interface.h @@ -3,9 +3,10 @@ struct strbuf; -#define GPG_VERIFY_VERBOSE 1 -#define GPG_VERIFY_RAW 2 -#define GPG_VERIFY_OMIT_STATUS 4 +#define GPG_VERIFY_VERBOSE (1<<0) +#define GPG_VERIFY_RAW (1<<1) +#define GPG_VERIFY_OMIT_STATUS (1<<2) +#define GPG_VERIFY_SUMMARY (1<<3) enum signature_trust_level { TRUST_UNDEFINED, @@ -42,6 +43,13 @@ struct signature_check { char *key; char *fingerprint; char *primary_key_fingerprint; + + /* "openpgp", "x509", "ssh" */ + char *format_name; + + /* hash algo for GPG/GPGSM, key type for SSH */ + char *sig_algo; + enum signature_trust_level trust_level; }; @@ -91,5 +99,7 @@ int check_signature(struct signature_check *sigc, const char *signature, size_t slen); void print_signature_buffer(const struct signature_check *sigc, unsigned flags); +void print_signature_summary(const struct signature_check *sigc, + unsigned flags); #endif diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh index 39677e859ab31173f7eaa36df1a0d3ecfe285abf..47f40862f3cfadd26ae47586f8ad0684d3c9b335 100755 --- a/t/t7510-signed-commit.sh +++ b/t/t7510-signed-commit.sh @@ -232,6 +232,30 @@ test_expect_success GPG2 'bare signature' ' test_cmp expect actual ' +test_expect_success GPG 'verify signatures with --summary' ' + # GPG-signed commit + git verify-commit --summary sixth-signed >actual && + test_grep "^G openpgp sha1" actual && + + # Non-signed commit + test_must_fail git verify-commit --summary seventh-unsigned >actual 2>&1 && + test_grep "^N ? ?" actual && + + # Trusted signature with alternate key (hash used might depend on the OS) + git verify-commit --summary eighth-signed-alt >actual && + test_grep -E "^G openpgp sha(256|512)" actual && + + # Bad signature + test_must_fail git verify-commit --summary $(cat forged1.commit) >actual 2>err && + test_grep "^B openpgp ?" actual +' + +test_expect_success GPG '--summary and --raw work together' ' + git verify-commit --summary --raw sixth-signed >actual 2>err && + test_grep "^G openpgp sha1" actual && + test_grep "GOODSIG" err +' + test_expect_success GPG 'show good signature with custom format' ' cat >expect <<-\EOF && G diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh index 065f78063629cbfad15e9360836544da34a16484..3d0e7d7859259a99506297c3e5cf3c51cb4ccc29 100755 --- a/t/t7528-signed-commit-ssh.sh +++ b/t/t7528-signed-commit-ssh.sh @@ -277,6 +277,34 @@ test_expect_success GPGSSH 'detect fudged signature with NUL' ' ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual2 ' +test_expect_success GPGSSH 'verify-commit --summary outputs format and key type for SSH signatures' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + + # SSH-signed commit with ED25519 key + git verify-commit --summary sixth-signed >actual && + test_grep "^G ssh ED25519" actual && + + # SSH-signed commit with ECDSA key + git verify-commit --summary thirteenth-signed-ecdsa >actual && + test_grep "^G ssh ECDSA" actual && + + # Non-signed commit + test_must_fail git verify-commit --summary seventh-unsigned >actual 2>&1 && + test_grep "^N ? ?" actual && + + # Bad signature + test_must_fail git verify-commit --summary $(cat forged1.commit) >actual 2>err && + test_grep "^B ssh ?" actual +' + +test_expect_success GPGSSH '--summary and --raw work together' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + + git verify-commit --summary --raw sixth-signed >actual 2>err && + test_grep "^G ssh ED25519" actual && + test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" err +' + test_expect_success GPGSSH 'amending already signed commit' ' test_config gpg.format ssh && test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&