Merge branch 'fs/ssh-signing-key-lifetime'

Extend the signing of objects with SSH keys and learn to pay
attention to the key validity time range when verifying.

* fs/ssh-signing-key-lifetime:
  ssh signing: verify ssh-keygen in test prereq
  ssh signing: make fmt-merge-msg consider key lifetime
  ssh signing: make verify-tag consider key lifetime
  ssh signing: make git log verify key lifetime
  ssh signing: make verify-commit consider key lifetime
  ssh signing: add key lifetime test prereqs
  ssh signing: use sigc struct to pass payload
  t/fmt-merge-msg: make gpgssh tests more specific
  t/fmt-merge-msg: do not redirect stderr
This commit is contained in:
Junio C Hamano 2021-12-21 15:03:15 -08:00
commit d2f0b72759
13 changed files with 351 additions and 48 deletions

View file

@ -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. 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. 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 Using a SSH CA key with the cert-authority option
(see ssh-keygen(1) "CERTIFICATES") is also valid. (see ssh-keygen(1) "CERTIFICATES") is also valid.

View file

@ -769,8 +769,10 @@ static void prepare_push_cert_sha1(struct child_process *proc)
memset(&sigcheck, '\0', sizeof(sigcheck)); memset(&sigcheck, '\0', sizeof(sigcheck));
bogs = parse_signed_buffer(push_cert.buf, push_cert.len); bogs = parse_signed_buffer(push_cert.buf, push_cert.len);
check_signature(push_cert.buf, bogs, push_cert.buf + bogs, sigcheck.payload = xmemdupz(push_cert.buf, bogs);
push_cert.len - bogs, &sigcheck); sigcheck.payload_len = bogs;
check_signature(&sigcheck, push_cert.buf + bogs,
push_cert.len - bogs);
nonce_status = check_nonce(push_cert.buf, bogs); nonce_status = check_nonce(push_cert.buf, bogs);
} }

View file

@ -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) if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0)
goto out; 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: out:
strbuf_release(&payload); strbuf_release(&payload);

View file

@ -533,8 +533,9 @@ static void fmt_merge_msg_sigs(struct strbuf *out)
else { else {
buf = payload.buf; buf = payload.buf;
len = payload.len; len = payload.len;
if (check_signature(payload.buf, payload.len, sig.buf, sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
sig.len, &sigc) && sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
if (check_signature(&sigc, sig.buf, sig.len) &&
!sigc.output) !sigc.output)
strbuf_addstr(&sig, "gpg verification failed.\n"); strbuf_addstr(&sig, "gpg verification failed.\n");
else else

View file

@ -19,8 +19,8 @@ struct gpg_format {
const char **verify_args; const char **verify_args;
const char **sigs; const char **sigs;
int (*verify_signed_buffer)(struct signature_check *sigc, int (*verify_signed_buffer)(struct signature_check *sigc,
struct gpg_format *fmt, const char *payload, struct gpg_format *fmt,
size_t payload_size, const char *signature, const char *signature,
size_t signature_size); size_t signature_size);
int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature, int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,
const char *signing_key); const char *signing_key);
@ -53,12 +53,12 @@ static const char *ssh_sigs[] = {
}; };
static int verify_gpg_signed_buffer(struct signature_check *sigc, static int verify_gpg_signed_buffer(struct signature_check *sigc,
struct gpg_format *fmt, const char *payload, struct gpg_format *fmt,
size_t payload_size, const char *signature, const char *signature,
size_t signature_size); size_t signature_size);
static int verify_ssh_signed_buffer(struct signature_check *sigc, static int verify_ssh_signed_buffer(struct signature_check *sigc,
struct gpg_format *fmt, const char *payload, struct gpg_format *fmt,
size_t payload_size, const char *signature, const char *signature,
size_t signature_size); size_t signature_size);
static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
const char *signing_key); 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, static int verify_gpg_signed_buffer(struct signature_check *sigc,
struct gpg_format *fmt, const char *payload, struct gpg_format *fmt,
size_t payload_size, const char *signature, const char *signature,
size_t signature_size) size_t signature_size)
{ {
struct child_process gpg = CHILD_PROCESS_INIT; struct child_process gpg = CHILD_PROCESS_INIT;
@ -343,14 +343,13 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
NULL); NULL);
sigchain_push(SIGPIPE, SIG_IGN); 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); &gpg_stderr, 0);
sigchain_pop(SIGPIPE); sigchain_pop(SIGPIPE);
delete_tempfile(&temp); delete_tempfile(&temp);
ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
sigc->payload = xmemdupz(payload, payload_size);
sigc->output = strbuf_detach(&gpg_stderr, NULL); sigc->output = strbuf_detach(&gpg_stderr, NULL);
sigc->gpg_status = strbuf_detach(&gpg_stdout, 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, static int verify_ssh_signed_buffer(struct signature_check *sigc,
struct gpg_format *fmt, const char *payload, struct gpg_format *fmt,
size_t payload_size, const char *signature, const char *signature,
size_t signature_size) size_t signature_size)
{ {
struct child_process ssh_keygen = CHILD_PROCESS_INIT; 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_principals_err = STRBUF_INIT;
struct strbuf ssh_keygen_out = STRBUF_INIT; struct strbuf ssh_keygen_out = STRBUF_INIT;
struct strbuf ssh_keygen_err = 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) { if (!ssh_allowed_signers) {
error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification")); 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; 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 */ /* Find the principal from the signers */
strvec_pushl(&ssh_keygen.args, fmt->program, strvec_pushl(&ssh_keygen.args, fmt->program,
"-Y", "find-principals", "-Y", "find-principals",
"-f", ssh_allowed_signers, "-f", ssh_allowed_signers,
"-s", buffer_file->filename.buf, "-s", buffer_file->filename.buf,
verify_time.buf,
NULL); NULL);
ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0, ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0,
&ssh_principals_err, 0); &ssh_principals_err, 0);
@ -479,8 +490,9 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
"-Y", "check-novalidate", "-Y", "check-novalidate",
"-n", "git", "-n", "git",
"-s", buffer_file->filename.buf, "-s", buffer_file->filename.buf,
verify_time.buf,
NULL); 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); &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, "-f", ssh_allowed_signers,
"-I", principal, "-I", principal,
"-s", buffer_file->filename.buf, "-s", buffer_file->filename.buf,
verify_time.buf,
NULL); NULL);
if (ssh_revocation_file) { if (ssh_revocation_file) {
@ -526,7 +539,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
} }
sigchain_push(SIGPIPE, SIG_IGN); 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); &ssh_keygen_out, 0, &ssh_keygen_err, 0);
sigchain_pop(SIGPIPE); 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_out, 0);
strbuf_stripspace(&ssh_keygen_err, 0); strbuf_stripspace(&ssh_keygen_err, 0);
/* Add stderr outputs to show the user actual ssh-keygen errors */ /* 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_principals_err);
strbuf_release(&ssh_keygen_out); strbuf_release(&ssh_keygen_out);
strbuf_release(&ssh_keygen_err); strbuf_release(&ssh_keygen_err);
strbuf_release(&verify_time);
return ret; return ret;
} }
int check_signature(const char *payload, size_t plen, const char *signature, static int parse_payload_metadata(struct signature_check *sigc)
size_t slen, 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; struct gpg_format *fmt;
int status; int status;
@ -575,8 +623,10 @@ int check_signature(const char *payload, size_t plen, const char *signature,
if (!fmt) if (!fmt)
die(_("bad/incompatible signature '%s'"), signature); die(_("bad/incompatible signature '%s'"), signature);
status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature, if (parse_payload_metadata(sigc))
slen); return 1;
status = fmt->verify_signed_buffer(sigc, fmt, signature, slen);
if (status && !sigc->output) if (status && !sigc->output)
return !!status; return !!status;
@ -593,7 +643,7 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags)
sigc->output; sigc->output;
if (flags & GPG_VERIFY_VERBOSE && sigc->payload) if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
fputs(sigc->payload, stdout); fwrite(sigc->payload, 1, sigc->payload_len, stdout);
if (output) if (output)
fputs(output, stderr); fputs(output, stderr);

View file

@ -15,8 +15,18 @@ enum signature_trust_level {
TRUST_ULTIMATE, TRUST_ULTIMATE,
}; };
enum payload_type {
SIGNATURE_PAYLOAD_UNDEFINED,
SIGNATURE_PAYLOAD_COMMIT,
SIGNATURE_PAYLOAD_TAG,
SIGNATURE_PAYLOAD_PUSH_CERT,
};
struct signature_check { struct signature_check {
char *payload; char *payload;
size_t payload_len;
enum payload_type payload_type;
timestamp_t payload_timestamp;
char *output; char *output;
char *gpg_status; char *gpg_status;
@ -70,9 +80,8 @@ const char *get_signing_key(void);
* Either a GPG KeyID or a SSH Key Fingerprint * Either a GPG KeyID or a SSH Key Fingerprint
*/ */
const char *get_signing_key_id(void); const char *get_signing_key_id(void);
int check_signature(const char *payload, size_t plen, int check_signature(struct signature_check *sigc,
const char *signature, size_t slen, const char *signature, size_t slen);
struct signature_check *sigc);
void print_signature_buffer(const struct signature_check *sigc, void print_signature_buffer(const struct signature_check *sigc,
unsigned flags); unsigned flags);

View file

@ -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) if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0)
goto out; goto out;
status = check_signature(payload.buf, payload.len, signature.buf, sigc.payload_type = SIGNATURE_PAYLOAD_COMMIT;
signature.len, &sigc); sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
status = check_signature(&sigc, signature.buf, signature.len);
if (status && !sigc.output) if (status && !sigc.output)
show_sig_lines(opt, status, "No signature\n"); show_sig_lines(opt, status, "No signature\n");
else else
@ -583,8 +584,9 @@ static int show_one_mergetag(struct commit *commit,
status = -1; status = -1;
if (parse_signature(extra->value, extra->len, &payload, &signature)) { if (parse_signature(extra->value, extra->len, &payload, &signature)) {
/* could have a good signature */ /* could have a good signature */
status = check_signature(payload.buf, payload.len, sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
signature.buf, signature.len, &sigc); sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
status = check_signature(&sigc, signature.buf, signature.len);
if (sigc.output) if (sigc.output)
strbuf_addstr(&verify_message, sigc.output); strbuf_addstr(&verify_message, sigc.output);
else else

View file

@ -90,6 +90,10 @@ test_lazy_prereq RFC1991 '
GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key" GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key"
GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key" GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key"
GPGSSH_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_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_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key"
GPGSSH_KEY_PASSPHRASE="super_secret" GPGSSH_KEY_PASSPHRASE="super_secret"
GPGSSH_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile" 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" echo $ssh_version | grep -q "find-principals:missing signature file"
test $? = 0 || exit 1; test $? = 0 || exit 1;
# some broken versions of ssh-keygen segfault on find-principals; # Setup some keys and an allowed signers file
# avoid testing with them.
ssh-keygen -Y find-principals -f /dev/null -s /dev/null
test $? = 139 && exit 1
mkdir -p "${GNUPGHOME}" && mkdir -p "${GNUPGHOME}" &&
chmod 0700 "${GNUPGHOME}" && chmod 0700 "${GNUPGHOME}" &&
(setfacl -k "${GNUPGHOME}" 2>/dev/null || true) && (setfacl -k "${GNUPGHOME}" 2>/dev/null || true) &&
ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null && 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 && 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 && 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 "" -C "git ed25519 key" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null &&
ssh-keygen -t ed25519 -N "" -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() { sanitize_pgp() {

View file

@ -1714,6 +1714,24 @@ test_expect_success GPGSSH 'setup sshkey signed branch' '
git commit -S -m signed_commit 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' ' test_expect_success GPGSM 'log x509 fingerprint' '
echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect &&
git log -n1 --format="%GF | %GP" signed-x509 >actual && 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 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_expect_success GPG 'log --graph --show-signature for merged tag' '
test_when_finished "git reset --hard && git checkout main" && test_when_finished "git reset --hard && git checkout main" &&
git checkout -b plain main && git checkout -b plain main &&

View file

@ -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 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' ' test_expect_success 'message for merging local branch' '
echo "Merge branch ${apos}left${apos}" >expected && 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' ' test_expect_success GPG 'message for merging local tag signed by good key' '
git checkout main && git checkout main &&
git fetch . signed-good-tag && 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 "^Merge tag ${apos}signed-good-tag${apos}" actual &&
grep "^# gpg: Signature made" actual && grep "^# gpg: Signature made" actual &&
grep "^# gpg: Good signature from" 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' ' test_expect_success GPG 'message for merging local tag signed by unknown key' '
git checkout main && git checkout main &&
git fetch . signed-good-tag && 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 "^Merge tag ${apos}signed-good-tag${apos}" actual &&
grep "^# gpg: Signature made" actual && grep "^# gpg: Signature made" actual &&
grep -E "^# gpg: Can${apos}t check signature: (public key not found|No public key)" 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}" && test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
git checkout main && git checkout main &&
git fetch . signed-good-ssh-tag && 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_GOOD_SIGNATURE_TRUSTED}" actual &&
! grep "${GPGSSH_BAD_SIGNATURE}" 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}" && test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
git checkout main && git checkout main &&
git fetch . signed-untrusted-ssh-tag && 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_GOOD_SIGNATURE_UNTRUSTED}" actual &&
! grep "${GPGSSH_BAD_SIGNATURE}" actual && ! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
grep "${GPGSSH_KEY_NOT_TRUSTED}" 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' ' test_expect_success 'message for merging external branch' '
echo "Merge branch ${apos}left${apos} of $(pwd)" >expected && echo "Merge branch ${apos}left${apos} of $(pwd)" >expected &&

View file

@ -48,6 +48,23 @@ test_expect_success GPGSSH 'create signed tags ssh' '
git tag -u"${GPGSSH_KEY_UNTRUSTED}" -m eighth eighth-signed-alt 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_expect_success GPGSSH 'verify and show ssh signatures' '
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && 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_expect_success GPGSSH 'detect fudged ssh signature' '
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
git cat-file tag seventh-signed >raw && git cat-file tag seventh-signed >raw &&

View file

@ -76,6 +76,23 @@ test_expect_success GPGSSH 'create signed commits' '
git tag twelfth-signed-alt $(cat oid) 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_expect_success GPGSSH 'verify and show signatures' '
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
test_config gpg.mintrustlevel UNDEFINED && 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 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_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '
test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
test_config gpg.minTrustLevel fully && test_config gpg.minTrustLevel fully &&

5
tag.c
View file

@ -25,8 +25,9 @@ static int run_gpg_verify(const char *buf, unsigned long size, unsigned flags)
return error("no signature found"); return error("no signature found");
} }
ret = check_signature(payload.buf, payload.len, signature.buf, sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
signature.len, &sigc); sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
ret = check_signature(&sigc, signature.buf, signature.len);
if (!(flags & GPG_VERIFY_OMIT_STATUS)) if (!(flags & GPG_VERIFY_OMIT_STATUS))
print_signature_buffer(&sigc, flags); print_signature_buffer(&sigc, flags);