diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt index 4f30c7dbdd..c9be554c73 100644 --- a/Documentation/config/gpg.txt +++ b/Documentation/config/gpg.txt @@ -64,6 +64,11 @@ A repository that only allows signed commits can store the file in the repository itself using a path relative to the top-level of the working tree. This way only committers with an already valid key can add or change keys in the keyring. + +Since OpensSSH 8.8 this file allows specifying a key lifetime using valid-after & +valid-before options. Git will mark signatures as valid if the signing key was +valid at the time of the signatures creation. This allows users to change a +signing key without invalidating all previously made signatures. ++ Using a SSH CA key with the cert-authority option (see ssh-keygen(1) "CERTIFICATES") is also valid. diff --git a/commit.c b/commit.c index 64e040a99b..a348f085b2 100644 --- a/commit.c +++ b/commit.c @@ -1213,6 +1213,7 @@ int check_commit_signature(const struct commit *commit, struct signature_check * if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0) goto out; + sigc->payload_type = SIGNATURE_PAYLOAD_COMMIT; sigc->payload = strbuf_detach(&payload, &sigc->payload_len); ret = check_signature(sigc, signature.buf, signature.len); diff --git a/gpg-interface.c b/gpg-interface.c index 75ab6faacb..330cfc5845 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -439,6 +439,13 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, struct strbuf ssh_principals_err = STRBUF_INIT; struct strbuf ssh_keygen_out = STRBUF_INIT; struct strbuf ssh_keygen_err = STRBUF_INIT; + struct strbuf verify_time = STRBUF_INIT; + const struct date_mode verify_date_mode = { + .type = DATE_STRFTIME, + .strftime_fmt = "%Y%m%d%H%M%S", + /* SSH signing key validity has no timezone information - Use the local timezone */ + .local = 1, + }; if (!ssh_allowed_signers) { error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification")); @@ -456,11 +463,16 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, return -1; } + if (sigc->payload_timestamp) + strbuf_addf(&verify_time, "-Overify-time=%s", + show_date(sigc->payload_timestamp, 0, &verify_date_mode)); + /* Find the principal from the signers */ strvec_pushl(&ssh_keygen.args, fmt->program, "-Y", "find-principals", "-f", ssh_allowed_signers, "-s", buffer_file->filename.buf, + verify_time.buf, NULL); ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0, &ssh_principals_err, 0); @@ -478,6 +490,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, "-Y", "check-novalidate", "-n", "git", "-s", buffer_file->filename.buf, + verify_time.buf, NULL); pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len, &ssh_keygen_out, 0, &ssh_keygen_err, 0); @@ -512,6 +525,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, "-f", ssh_allowed_signers, "-I", principal, "-s", buffer_file->filename.buf, + verify_time.buf, NULL); if (ssh_revocation_file) { @@ -556,10 +570,46 @@ out: strbuf_release(&ssh_principals_err); strbuf_release(&ssh_keygen_out); strbuf_release(&ssh_keygen_err); + strbuf_release(&verify_time); return ret; } +static int parse_payload_metadata(struct signature_check *sigc) +{ + const char *ident_line = NULL; + size_t ident_len; + struct ident_split ident; + const char *signer_header; + + switch (sigc->payload_type) { + case SIGNATURE_PAYLOAD_COMMIT: + signer_header = "committer"; + break; + case SIGNATURE_PAYLOAD_TAG: + signer_header = "tagger"; + break; + case SIGNATURE_PAYLOAD_UNDEFINED: + case SIGNATURE_PAYLOAD_PUSH_CERT: + /* Ignore payloads we don't want to parse */ + return 0; + default: + BUG("invalid value for sigc->payload_type"); + } + + ident_line = find_commit_header(sigc->payload, signer_header, &ident_len); + if (!ident_line || !ident_len) + return 1; + + if (split_ident_line(&ident, ident_line, ident_len)) + return 1; + + if (!sigc->payload_timestamp && ident.date_begin && ident.date_end) + sigc->payload_timestamp = parse_timestamp(ident.date_begin, NULL, 10); + + return 0; +} + int check_signature(struct signature_check *sigc, const char *signature, size_t slen) { @@ -573,6 +623,9 @@ int check_signature(struct signature_check *sigc, if (!fmt) die(_("bad/incompatible signature '%s'"), signature); + if (parse_payload_metadata(sigc)) + return 1; + status = fmt->verify_signed_buffer(sigc, fmt, signature, slen); if (status && !sigc->output) diff --git a/gpg-interface.h b/gpg-interface.h index 5ee7d8b6b9..b30cbdcd3d 100644 --- a/gpg-interface.h +++ b/gpg-interface.h @@ -15,9 +15,18 @@ enum signature_trust_level { TRUST_ULTIMATE, }; +enum payload_type { + SIGNATURE_PAYLOAD_UNDEFINED, + SIGNATURE_PAYLOAD_COMMIT, + SIGNATURE_PAYLOAD_TAG, + SIGNATURE_PAYLOAD_PUSH_CERT, +}; + struct signature_check { char *payload; size_t payload_len; + enum payload_type payload_type; + timestamp_t payload_timestamp; char *output; char *gpg_status; diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh index badf3ed320..dae76ded0c 100755 --- a/t/t7528-signed-commit-ssh.sh +++ b/t/t7528-signed-commit-ssh.sh @@ -76,6 +76,23 @@ test_expect_success GPGSSH 'create signed commits' ' git tag twelfth-signed-alt $(cat oid) ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed commits with keys having defined lifetimes' ' + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + + echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" && + git tag expired-signed && + + echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" && + git tag notyetvalid-signed && + + echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" && + git tag timeboxedvalid-signed && + + echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" && + git tag timeboxedinvalid-signed +' + test_expect_success GPGSSH 'verify and show signatures' ' test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && test_config gpg.mintrustlevel UNDEFINED && @@ -122,6 +139,31 @@ test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature' grep "${GPGSSH_KEY_NOT_TRUSTED}" actual ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on expired signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-commit expired-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure on not yet valid signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-commit notyetvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit succeeds with commit date and key validity matching' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git verify-commit timeboxedvalid-signed 2>actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-commit exits failure with commit date outside of key validity' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-commit timeboxedinvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' ' test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && test_config gpg.minTrustLevel fully &&