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/builtin/receive-pack.c b/builtin/receive-pack.c index 313b372a11..07a3053c1d 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -769,8 +769,10 @@ static void prepare_push_cert_sha1(struct child_process *proc) memset(&sigcheck, '\0', sizeof(sigcheck)); bogs = parse_signed_buffer(push_cert.buf, push_cert.len); - check_signature(push_cert.buf, bogs, push_cert.buf + bogs, - push_cert.len - bogs, &sigcheck); + sigcheck.payload = xmemdupz(push_cert.buf, bogs); + sigcheck.payload_len = bogs; + check_signature(&sigcheck, push_cert.buf + bogs, + push_cert.len - bogs); nonce_status = check_nonce(push_cert.buf, bogs); } diff --git a/commit.c b/commit.c index 551de4903c..a348f085b2 100644 --- a/commit.c +++ b/commit.c @@ -1212,8 +1212,10 @@ int check_commit_signature(const struct commit *commit, struct signature_check * if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0) goto out; - ret = check_signature(payload.buf, payload.len, signature.buf, - signature.len, sigc); + + sigc->payload_type = SIGNATURE_PAYLOAD_COMMIT; + sigc->payload = strbuf_detach(&payload, &sigc->payload_len); + ret = check_signature(sigc, signature.buf, signature.len); out: strbuf_release(&payload); diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c index 5216191488..e4f7810be2 100644 --- a/fmt-merge-msg.c +++ b/fmt-merge-msg.c @@ -533,8 +533,9 @@ static void fmt_merge_msg_sigs(struct strbuf *out) else { buf = payload.buf; len = payload.len; - if (check_signature(payload.buf, payload.len, sig.buf, - sig.len, &sigc) && + sigc.payload_type = SIGNATURE_PAYLOAD_TAG; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + if (check_signature(&sigc, sig.buf, sig.len) && !sigc.output) strbuf_addstr(&sig, "gpg verification failed.\n"); else diff --git a/gpg-interface.c b/gpg-interface.c index 3e7255a2a9..330cfc5845 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -19,8 +19,8 @@ struct gpg_format { const char **verify_args; const char **sigs; int (*verify_signed_buffer)(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size); int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature, const char *signing_key); @@ -53,12 +53,12 @@ static const char *ssh_sigs[] = { }; static int verify_gpg_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size); static int verify_ssh_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size); static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, const char *signing_key); @@ -314,8 +314,8 @@ static void parse_gpg_output(struct signature_check *sigc) } static int verify_gpg_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size) { struct child_process gpg = CHILD_PROCESS_INIT; @@ -343,14 +343,13 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc, NULL); sigchain_push(SIGPIPE, SIG_IGN); - ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0, + ret = pipe_command(&gpg, sigc->payload, sigc->payload_len, &gpg_stdout, 0, &gpg_stderr, 0); sigchain_pop(SIGPIPE); delete_tempfile(&temp); ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); - sigc->payload = xmemdupz(payload, payload_size); sigc->output = strbuf_detach(&gpg_stderr, NULL); sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL); @@ -426,8 +425,8 @@ static void parse_ssh_output(struct signature_check *sigc) } static int verify_ssh_signed_buffer(struct signature_check *sigc, - struct gpg_format *fmt, const char *payload, - size_t payload_size, const char *signature, + struct gpg_format *fmt, + const char *signature, size_t signature_size) { struct child_process ssh_keygen = CHILD_PROCESS_INIT; @@ -440,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")); @@ -457,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); @@ -479,8 +490,9 @@ 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, payload, payload_size, + pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len, &ssh_keygen_out, 0, &ssh_keygen_err, 0); /* @@ -513,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) { @@ -526,7 +539,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, } sigchain_push(SIGPIPE, SIG_IGN); - ret = pipe_command(&ssh_keygen, payload, payload_size, + ret = pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len, &ssh_keygen_out, 0, &ssh_keygen_err, 0); sigchain_pop(SIGPIPE); @@ -540,7 +553,6 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, } } - sigc->payload = xmemdupz(payload, payload_size); strbuf_stripspace(&ssh_keygen_out, 0); strbuf_stripspace(&ssh_keygen_err, 0); /* Add stderr outputs to show the user actual ssh-keygen errors */ @@ -558,12 +570,48 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc, strbuf_release(&ssh_principals_err); strbuf_release(&ssh_keygen_out); strbuf_release(&ssh_keygen_err); + strbuf_release(&verify_time); return ret; } -int check_signature(const char *payload, size_t plen, const char *signature, - size_t slen, struct signature_check *sigc) +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) { struct gpg_format *fmt; int status; @@ -575,8 +623,10 @@ int check_signature(const char *payload, size_t plen, const char *signature, if (!fmt) die(_("bad/incompatible signature '%s'"), signature); - status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature, - slen); + if (parse_payload_metadata(sigc)) + return 1; + + status = fmt->verify_signed_buffer(sigc, fmt, signature, slen); if (status && !sigc->output) return !!status; @@ -593,7 +643,7 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags) sigc->output; if (flags & GPG_VERIFY_VERBOSE && sigc->payload) - fputs(sigc->payload, stdout); + fwrite(sigc->payload, 1, sigc->payload_len, stdout); if (output) fputs(output, stderr); diff --git a/gpg-interface.h b/gpg-interface.h index beefacbb1e..b30cbdcd3d 100644 --- a/gpg-interface.h +++ b/gpg-interface.h @@ -15,8 +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; @@ -70,9 +80,8 @@ const char *get_signing_key(void); * Either a GPG KeyID or a SSH Key Fingerprint */ const char *get_signing_key_id(void); -int check_signature(const char *payload, size_t plen, - const char *signature, size_t slen, - struct signature_check *sigc); +int check_signature(struct signature_check *sigc, + const char *signature, size_t slen); void print_signature_buffer(const struct signature_check *sigc, unsigned flags); diff --git a/log-tree.c b/log-tree.c index 644893fd8c..d3e7a40b64 100644 --- a/log-tree.c +++ b/log-tree.c @@ -513,8 +513,9 @@ static void show_signature(struct rev_info *opt, struct commit *commit) if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0) goto out; - status = check_signature(payload.buf, payload.len, signature.buf, - signature.len, &sigc); + sigc.payload_type = SIGNATURE_PAYLOAD_COMMIT; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + status = check_signature(&sigc, signature.buf, signature.len); if (status && !sigc.output) show_sig_lines(opt, status, "No signature\n"); else @@ -583,8 +584,9 @@ static int show_one_mergetag(struct commit *commit, status = -1; if (parse_signature(extra->value, extra->len, &payload, &signature)) { /* could have a good signature */ - status = check_signature(payload.buf, payload.len, - signature.buf, signature.len, &sigc); + sigc.payload_type = SIGNATURE_PAYLOAD_TAG; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + status = check_signature(&sigc, signature.buf, signature.len); if (sigc.output) strbuf_addstr(&verify_message, sigc.output); else diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh index a3f285f515..3fadfcb306 100644 --- a/t/lib-gpg.sh +++ b/t/lib-gpg.sh @@ -90,6 +90,10 @@ test_lazy_prereq RFC1991 ' GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key" GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key" GPGSSH_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key" +GPGSSH_KEY_EXPIRED="${GNUPGHOME}/expired_ssh_signing_key" +GPGSSH_KEY_NOTYETVALID="${GNUPGHOME}/notyetvalid_ssh_signing_key" +GPGSSH_KEY_TIMEBOXEDVALID="${GNUPGHOME}/timeboxed_valid_ssh_signing_key" +GPGSSH_KEY_TIMEBOXEDINVALID="${GNUPGHOME}/timeboxed_invalid_ssh_signing_key" GPGSSH_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key" GPGSSH_KEY_PASSPHRASE="super_secret" GPGSSH_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile" @@ -105,21 +109,61 @@ test_lazy_prereq GPGSSH ' echo $ssh_version | grep -q "find-principals:missing signature file" test $? = 0 || exit 1; - # some broken versions of ssh-keygen segfault on find-principals; - # avoid testing with them. - ssh-keygen -Y find-principals -f /dev/null -s /dev/null - test $? = 139 && exit 1 - + # Setup some keys and an allowed signers file mkdir -p "${GNUPGHOME}" && chmod 0700 "${GNUPGHOME}" && (setfacl -k "${GNUPGHOME}" 2>/dev/null || true) && ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null && - echo "\"principal with number 1\" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" && ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null && - echo "\"principal with number 2\" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" && ssh-keygen -t ed25519 -N "${GPGSSH_KEY_PASSPHRASE}" -C "git ed25519 encrypted key" -f "${GPGSSH_KEY_WITH_PASSPHRASE}" >/dev/null && - echo "\"principal with number 3\" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" && - ssh-keygen -t ed25519 -N "" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null + ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null && + + cat >"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF && + "principal with number 1" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" + "principal with number 2" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" + "principal with number 3" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" + EOF + + # Verify if at least one key and ssh-keygen works as expected + echo "testpayload" | + ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_PRIMARY}" >gpgssh_prereq.sig && + ssh-keygen -Y find-principals -f "${GPGSSH_ALLOWED_SIGNERS}" -s gpgssh_prereq.sig && + echo "testpayload" | + ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with number 1" -s gpgssh_prereq.sig +' + +test_lazy_prereq GPGSSH_VERIFYTIME ' + # Check if ssh-keygen has a verify-time option by passing an invalid date to it + ssh-keygen -Overify-time=INVALID -Y check-novalidate -s doesnotmatter 2>&1 | grep -q -F "Invalid \"verify-time\"" && + + # Set up keys with key lifetimes + ssh-keygen -t ed25519 -N "" -C "timeboxed valid key" -f "${GPGSSH_KEY_TIMEBOXEDVALID}" >/dev/null && + key_valid=$(cat "${GPGSSH_KEY_TIMEBOXEDVALID}.pub") && + ssh-keygen -t ed25519 -N "" -C "timeboxed invalid key" -f "${GPGSSH_KEY_TIMEBOXEDINVALID}" >/dev/null && + key_invalid=$(cat "${GPGSSH_KEY_TIMEBOXEDINVALID}.pub") && + ssh-keygen -t ed25519 -N "" -C "expired key" -f "${GPGSSH_KEY_EXPIRED}" >/dev/null && + key_expired=$(cat "${GPGSSH_KEY_EXPIRED}.pub") && + ssh-keygen -t ed25519 -N "" -C "not yet valid key" -f "${GPGSSH_KEY_NOTYETVALID}" >/dev/null && + key_notyetvalid=$(cat "${GPGSSH_KEY_NOTYETVALID}.pub") && + + # Timestamps outside of test_tick span + ts2005a=20050401000000 ts2005b=200504020000 && + # Timestamps within test_tick span + ts2005c=20050407000000 ts2005d=200504100000 && + # Definitely not yet valid / expired timestamps + ts2000=20000101000000 ts2999=29990101000000 && + + cat >>"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF && + "timeboxed valid key" valid-after="$ts2005c",valid-before="$ts2005d" $key_valid" + "timeboxed invalid key" valid-after="$ts2005a",valid-before="$ts2005b" $key_invalid" + "principal with expired key" valid-before="$ts2000" $key_expired" + "principal with not yet valid key" valid-after="$ts2999" $key_notyetvalid" + EOF + + # and verify ssh-keygen verifies the key lifetime + echo "testpayload" | + ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_EXPIRED}" >gpgssh_verifytime_prereq.sig && + ! (ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with expired key" -s gpgssh_verifytime_prereq.sig) ' sanitize_pgp() { diff --git a/t/t4202-log.sh b/t/t4202-log.sh index 7a725b9a23..2ced7e9d81 100755 --- a/t/t4202-log.sh +++ b/t/t4202-log.sh @@ -1714,6 +1714,24 @@ test_expect_success GPGSSH 'setup sshkey signed branch' ' git commit -S -m signed_commit ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed commits with keys having defined lifetimes' ' + test_config gpg.format ssh && + touch file && + git add file && + + 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 GPGSM 'log x509 fingerprint' ' echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && git log -n1 --format="%GF | %GP" signed-x509 >actual && @@ -1751,6 +1769,31 @@ test_expect_success GPGSSH 'log --graph --show-signature ssh' ' grep "${GOOD_SIGNATURE_TRUSTED}" actual ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on expired signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 expired-signed >actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure on not yet valid signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 notyetvalid-signed >actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log show success with commit date and key validity matching' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 timeboxedvalid-signed >actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'log shows failure with commit date outside of key validity' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 timeboxedinvalid-signed >actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout main" && git checkout -b plain main && diff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh index 06c5fb5615..6e10a539ce 100755 --- a/t/t6200-fmt-merge-msg.sh +++ b/t/t6200-fmt-merge-msg.sh @@ -91,6 +91,26 @@ test_expect_success GPGSSH 'created ssh signed commit and tag' ' git tag -s -u"${GPGSSH_KEY_UNTRUSTED}" -m signed-ssh-tag-msg-untrusted signed-untrusted-ssh-tag left ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed tags with keys having defined lifetimes' ' + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + git checkout -b signed-expiry-ssh && + touch file && + git add file && + + echo expired >file && test_tick && git commit -a -m expired -S"${GPGSSH_KEY_EXPIRED}" && + git tag -s -u "${GPGSSH_KEY_EXPIRED}" -m expired-signed expired-signed && + + echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" && + git tag -s -u "${GPGSSH_KEY_NOTYETVALID}" -m notyetvalid-signed notyetvalid-signed && + + echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDVALID}" -m timeboxedvalid-signed timeboxedvalid-signed && + + echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDINVALID}" -m timeboxedinvalid-signed timeboxedinvalid-signed +' + test_expect_success 'message for merging local branch' ' echo "Merge branch ${apos}left${apos}" >expected && @@ -104,7 +124,7 @@ test_expect_success 'message for merging local branch' ' test_expect_success GPG 'message for merging local tag signed by good key' ' git checkout main && git fetch . signed-good-tag && - git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + git fmt-merge-msg <.git/FETCH_HEAD >actual && grep "^Merge tag ${apos}signed-good-tag${apos}" actual && grep "^# gpg: Signature made" actual && grep "^# gpg: Good signature from" actual @@ -113,7 +133,7 @@ test_expect_success GPG 'message for merging local tag signed by good key' ' test_expect_success GPG 'message for merging local tag signed by unknown key' ' git checkout main && git fetch . signed-good-tag && - GNUPGHOME=. git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + GNUPGHOME=. git fmt-merge-msg <.git/FETCH_HEAD >actual && grep "^Merge tag ${apos}signed-good-tag${apos}" actual && grep "^# gpg: Signature made" actual && grep -E "^# gpg: Can${apos}t check signature: (public key not found|No public key)" actual @@ -123,7 +143,8 @@ test_expect_success GPGSSH 'message for merging local tag signed by good ssh key test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && git checkout main && git fetch . signed-good-ssh-tag && - git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}signed-good-ssh-tag${apos}" actual && grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && ! grep "${GPGSSH_BAD_SIGNATURE}" actual ' @@ -132,11 +153,50 @@ test_expect_success GPGSSH 'message for merging local tag signed by unknown ssh test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && git checkout main && git fetch . signed-untrusted-ssh-tag && - git fmt-merge-msg <.git/FETCH_HEAD >actual 2>&1 && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}signed-untrusted-ssh-tag${apos}" actual && grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual && ! grep "${GPGSSH_BAD_SIGNATURE}" actual && grep "${GPGSSH_KEY_NOT_TRUSTED}" actual ' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by expired ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . expired-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}expired-signed${apos}" actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by not yet valid ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . notyetvalid-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}notyetvalid-signed${apos}" actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by valid timeboxed ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . timeboxedvalid-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}timeboxedvalid-signed${apos}" actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'message for merging local tag signed by invalid timeboxed ssh key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git checkout main && + git fetch . timeboxedinvalid-signed && + git fmt-merge-msg <.git/FETCH_HEAD >actual && + grep "^Merge tag ${apos}timeboxedinvalid-signed${apos}" actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success 'message for merging external branch' ' echo "Merge branch ${apos}left${apos} of $(pwd)" >expected && diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh index 06c9dd6c93..1cb36b9ab8 100755 --- a/t/t7031-verify-tag-signed-ssh.sh +++ b/t/t7031-verify-tag-signed-ssh.sh @@ -48,6 +48,23 @@ test_expect_success GPGSSH 'create signed tags ssh' ' git tag -u"${GPGSSH_KEY_UNTRUSTED}" -m eighth eighth-signed-alt ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'create signed tags 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 -s -u "${GPGSSH_KEY_EXPIRED}" -m expired-signed expired-signed && + + echo notyetvalid >file && test_tick && git commit -a -m notyetvalid -S"${GPGSSH_KEY_NOTYETVALID}" && + git tag -s -u "${GPGSSH_KEY_NOTYETVALID}" -m notyetvalid-signed notyetvalid-signed && + + echo timeboxedvalid >file && test_tick && git commit -a -m timeboxedvalid -S"${GPGSSH_KEY_TIMEBOXEDVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDVALID}" -m timeboxedvalid-signed timeboxedvalid-signed && + + echo timeboxedinvalid >file && test_tick && git commit -a -m timeboxedinvalid -S"${GPGSSH_KEY_TIMEBOXEDINVALID}" && + git tag -s -u "${GPGSSH_KEY_TIMEBOXEDINVALID}" -m timeboxedinvalid-signed timeboxedinvalid-signed +' + test_expect_success GPGSSH 'verify and show ssh signatures' ' test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && ( @@ -80,6 +97,31 @@ test_expect_success GPGSSH 'verify and show ssh signatures' ' ) ' +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on expired signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-tag expired-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag exits failure on not yet valid signature key' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-tag notyetvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag succeeds with tag date and key validity matching' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + git verify-tag timeboxedvalid-signed 2>actual && + grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${GPGSSH_BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH,GPGSSH_VERIFYTIME 'verify-tag failes with tag date outside of key validity' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + test_must_fail git verify-tag timeboxedinvalid-signed 2>actual && + ! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPGSSH 'detect fudged ssh signature' ' test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && git cat-file tag seventh-signed >raw && 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 && diff --git a/tag.c b/tag.c index 3e18a41841..dfbcd7fcc2 100644 --- a/tag.c +++ b/tag.c @@ -25,8 +25,9 @@ static int run_gpg_verify(const char *buf, unsigned long size, unsigned flags) return error("no signature found"); } - ret = check_signature(payload.buf, payload.len, signature.buf, - signature.len, &sigc); + sigc.payload_type = SIGNATURE_PAYLOAD_TAG; + sigc.payload = strbuf_detach(&payload, &sigc.payload_len); + ret = check_signature(&sigc, signature.buf, signature.len); if (!(flags & GPG_VERIFY_OMIT_STATUS)) print_signature_buffer(&sigc, flags);