Skip to content

Commit d2f0b72

Browse files
committed
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
2 parents 3770c21 + 50992f9 commit d2f0b72

13 files changed

+351
-48
lines changed

Documentation/config/gpg.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ A repository that only allows signed commits can store the file
6464
in the repository itself using a path relative to the top-level of the working tree.
6565
This way only committers with an already valid key can add or change keys in the keyring.
6666
+
67+
Since OpensSSH 8.8 this file allows specifying a key lifetime using valid-after &
68+
valid-before options. Git will mark signatures as valid if the signing key was
69+
valid at the time of the signatures creation. This allows users to change a
70+
signing key without invalidating all previously made signatures.
71+
+
6772
Using a SSH CA key with the cert-authority option
6873
(see ssh-keygen(1) "CERTIFICATES") is also valid.
6974

builtin/receive-pack.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,8 +769,10 @@ static void prepare_push_cert_sha1(struct child_process *proc)
769769
memset(&sigcheck, '\0', sizeof(sigcheck));
770770

771771
bogs = parse_signed_buffer(push_cert.buf, push_cert.len);
772-
check_signature(push_cert.buf, bogs, push_cert.buf + bogs,
773-
push_cert.len - bogs, &sigcheck);
772+
sigcheck.payload = xmemdupz(push_cert.buf, bogs);
773+
sigcheck.payload_len = bogs;
774+
check_signature(&sigcheck, push_cert.buf + bogs,
775+
push_cert.len - bogs);
774776

775777
nonce_status = check_nonce(push_cert.buf, bogs);
776778
}

commit.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,8 +1212,10 @@ int check_commit_signature(const struct commit *commit, struct signature_check *
12121212

12131213
if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0)
12141214
goto out;
1215-
ret = check_signature(payload.buf, payload.len, signature.buf,
1216-
signature.len, sigc);
1215+
1216+
sigc->payload_type = SIGNATURE_PAYLOAD_COMMIT;
1217+
sigc->payload = strbuf_detach(&payload, &sigc->payload_len);
1218+
ret = check_signature(sigc, signature.buf, signature.len);
12171219

12181220
out:
12191221
strbuf_release(&payload);

fmt-merge-msg.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,9 @@ static void fmt_merge_msg_sigs(struct strbuf *out)
533533
else {
534534
buf = payload.buf;
535535
len = payload.len;
536-
if (check_signature(payload.buf, payload.len, sig.buf,
537-
sig.len, &sigc) &&
536+
sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
537+
sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
538+
if (check_signature(&sigc, sig.buf, sig.len) &&
538539
!sigc.output)
539540
strbuf_addstr(&sig, "gpg verification failed.\n");
540541
else

gpg-interface.c

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ struct gpg_format {
1919
const char **verify_args;
2020
const char **sigs;
2121
int (*verify_signed_buffer)(struct signature_check *sigc,
22-
struct gpg_format *fmt, const char *payload,
23-
size_t payload_size, const char *signature,
22+
struct gpg_format *fmt,
23+
const char *signature,
2424
size_t signature_size);
2525
int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,
2626
const char *signing_key);
@@ -53,12 +53,12 @@ static const char *ssh_sigs[] = {
5353
};
5454

5555
static int verify_gpg_signed_buffer(struct signature_check *sigc,
56-
struct gpg_format *fmt, const char *payload,
57-
size_t payload_size, const char *signature,
56+
struct gpg_format *fmt,
57+
const char *signature,
5858
size_t signature_size);
5959
static int verify_ssh_signed_buffer(struct signature_check *sigc,
60-
struct gpg_format *fmt, const char *payload,
61-
size_t payload_size, const char *signature,
60+
struct gpg_format *fmt,
61+
const char *signature,
6262
size_t signature_size);
6363
static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
6464
const char *signing_key);
@@ -314,8 +314,8 @@ static void parse_gpg_output(struct signature_check *sigc)
314314
}
315315

316316
static int verify_gpg_signed_buffer(struct signature_check *sigc,
317-
struct gpg_format *fmt, const char *payload,
318-
size_t payload_size, const char *signature,
317+
struct gpg_format *fmt,
318+
const char *signature,
319319
size_t signature_size)
320320
{
321321
struct child_process gpg = CHILD_PROCESS_INIT;
@@ -343,14 +343,13 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
343343
NULL);
344344

345345
sigchain_push(SIGPIPE, SIG_IGN);
346-
ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,
346+
ret = pipe_command(&gpg, sigc->payload, sigc->payload_len, &gpg_stdout, 0,
347347
&gpg_stderr, 0);
348348
sigchain_pop(SIGPIPE);
349349

350350
delete_tempfile(&temp);
351351

352352
ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
353-
sigc->payload = xmemdupz(payload, payload_size);
354353
sigc->output = strbuf_detach(&gpg_stderr, NULL);
355354
sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);
356355

@@ -426,8 +425,8 @@ static void parse_ssh_output(struct signature_check *sigc)
426425
}
427426

428427
static int verify_ssh_signed_buffer(struct signature_check *sigc,
429-
struct gpg_format *fmt, const char *payload,
430-
size_t payload_size, const char *signature,
428+
struct gpg_format *fmt,
429+
const char *signature,
431430
size_t signature_size)
432431
{
433432
struct child_process ssh_keygen = CHILD_PROCESS_INIT;
@@ -440,6 +439,13 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
440439
struct strbuf ssh_principals_err = STRBUF_INIT;
441440
struct strbuf ssh_keygen_out = STRBUF_INIT;
442441
struct strbuf ssh_keygen_err = STRBUF_INIT;
442+
struct strbuf verify_time = STRBUF_INIT;
443+
const struct date_mode verify_date_mode = {
444+
.type = DATE_STRFTIME,
445+
.strftime_fmt = "%Y%m%d%H%M%S",
446+
/* SSH signing key validity has no timezone information - Use the local timezone */
447+
.local = 1,
448+
};
443449

444450
if (!ssh_allowed_signers) {
445451
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,
457463
return -1;
458464
}
459465

466+
if (sigc->payload_timestamp)
467+
strbuf_addf(&verify_time, "-Overify-time=%s",
468+
show_date(sigc->payload_timestamp, 0, &verify_date_mode));
469+
460470
/* Find the principal from the signers */
461471
strvec_pushl(&ssh_keygen.args, fmt->program,
462472
"-Y", "find-principals",
463473
"-f", ssh_allowed_signers,
464474
"-s", buffer_file->filename.buf,
475+
verify_time.buf,
465476
NULL);
466477
ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0,
467478
&ssh_principals_err, 0);
@@ -479,8 +490,9 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
479490
"-Y", "check-novalidate",
480491
"-n", "git",
481492
"-s", buffer_file->filename.buf,
493+
verify_time.buf,
482494
NULL);
483-
pipe_command(&ssh_keygen, payload, payload_size,
495+
pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len,
484496
&ssh_keygen_out, 0, &ssh_keygen_err, 0);
485497

486498
/*
@@ -513,6 +525,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
513525
"-f", ssh_allowed_signers,
514526
"-I", principal,
515527
"-s", buffer_file->filename.buf,
528+
verify_time.buf,
516529
NULL);
517530

518531
if (ssh_revocation_file) {
@@ -526,7 +539,7 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
526539
}
527540

528541
sigchain_push(SIGPIPE, SIG_IGN);
529-
ret = pipe_command(&ssh_keygen, payload, payload_size,
542+
ret = pipe_command(&ssh_keygen, sigc->payload, sigc->payload_len,
530543
&ssh_keygen_out, 0, &ssh_keygen_err, 0);
531544
sigchain_pop(SIGPIPE);
532545

@@ -540,7 +553,6 @@ static int verify_ssh_signed_buffer(struct signature_check *sigc,
540553
}
541554
}
542555

543-
sigc->payload = xmemdupz(payload, payload_size);
544556
strbuf_stripspace(&ssh_keygen_out, 0);
545557
strbuf_stripspace(&ssh_keygen_err, 0);
546558
/* 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,
558570
strbuf_release(&ssh_principals_err);
559571
strbuf_release(&ssh_keygen_out);
560572
strbuf_release(&ssh_keygen_err);
573+
strbuf_release(&verify_time);
561574

562575
return ret;
563576
}
564577

565-
int check_signature(const char *payload, size_t plen, const char *signature,
566-
size_t slen, struct signature_check *sigc)
578+
static int parse_payload_metadata(struct signature_check *sigc)
579+
{
580+
const char *ident_line = NULL;
581+
size_t ident_len;
582+
struct ident_split ident;
583+
const char *signer_header;
584+
585+
switch (sigc->payload_type) {
586+
case SIGNATURE_PAYLOAD_COMMIT:
587+
signer_header = "committer";
588+
break;
589+
case SIGNATURE_PAYLOAD_TAG:
590+
signer_header = "tagger";
591+
break;
592+
case SIGNATURE_PAYLOAD_UNDEFINED:
593+
case SIGNATURE_PAYLOAD_PUSH_CERT:
594+
/* Ignore payloads we don't want to parse */
595+
return 0;
596+
default:
597+
BUG("invalid value for sigc->payload_type");
598+
}
599+
600+
ident_line = find_commit_header(sigc->payload, signer_header, &ident_len);
601+
if (!ident_line || !ident_len)
602+
return 1;
603+
604+
if (split_ident_line(&ident, ident_line, ident_len))
605+
return 1;
606+
607+
if (!sigc->payload_timestamp && ident.date_begin && ident.date_end)
608+
sigc->payload_timestamp = parse_timestamp(ident.date_begin, NULL, 10);
609+
610+
return 0;
611+
}
612+
613+
int check_signature(struct signature_check *sigc,
614+
const char *signature, size_t slen)
567615
{
568616
struct gpg_format *fmt;
569617
int status;
@@ -575,8 +623,10 @@ int check_signature(const char *payload, size_t plen, const char *signature,
575623
if (!fmt)
576624
die(_("bad/incompatible signature '%s'"), signature);
577625

578-
status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
579-
slen);
626+
if (parse_payload_metadata(sigc))
627+
return 1;
628+
629+
status = fmt->verify_signed_buffer(sigc, fmt, signature, slen);
580630

581631
if (status && !sigc->output)
582632
return !!status;
@@ -593,7 +643,7 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags)
593643
sigc->output;
594644

595645
if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
596-
fputs(sigc->payload, stdout);
646+
fwrite(sigc->payload, 1, sigc->payload_len, stdout);
597647

598648
if (output)
599649
fputs(output, stderr);

gpg-interface.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@ enum signature_trust_level {
1515
TRUST_ULTIMATE,
1616
};
1717

18+
enum payload_type {
19+
SIGNATURE_PAYLOAD_UNDEFINED,
20+
SIGNATURE_PAYLOAD_COMMIT,
21+
SIGNATURE_PAYLOAD_TAG,
22+
SIGNATURE_PAYLOAD_PUSH_CERT,
23+
};
24+
1825
struct signature_check {
1926
char *payload;
27+
size_t payload_len;
28+
enum payload_type payload_type;
29+
timestamp_t payload_timestamp;
2030
char *output;
2131
char *gpg_status;
2232

@@ -70,9 +80,8 @@ const char *get_signing_key(void);
7080
* Either a GPG KeyID or a SSH Key Fingerprint
7181
*/
7282
const char *get_signing_key_id(void);
73-
int check_signature(const char *payload, size_t plen,
74-
const char *signature, size_t slen,
75-
struct signature_check *sigc);
83+
int check_signature(struct signature_check *sigc,
84+
const char *signature, size_t slen);
7685
void print_signature_buffer(const struct signature_check *sigc,
7786
unsigned flags);
7887

log-tree.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,9 @@ static void show_signature(struct rev_info *opt, struct commit *commit)
513513
if (parse_signed_commit(commit, &payload, &signature, the_hash_algo) <= 0)
514514
goto out;
515515

516-
status = check_signature(payload.buf, payload.len, signature.buf,
517-
signature.len, &sigc);
516+
sigc.payload_type = SIGNATURE_PAYLOAD_COMMIT;
517+
sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
518+
status = check_signature(&sigc, signature.buf, signature.len);
518519
if (status && !sigc.output)
519520
show_sig_lines(opt, status, "No signature\n");
520521
else
@@ -583,8 +584,9 @@ static int show_one_mergetag(struct commit *commit,
583584
status = -1;
584585
if (parse_signature(extra->value, extra->len, &payload, &signature)) {
585586
/* could have a good signature */
586-
status = check_signature(payload.buf, payload.len,
587-
signature.buf, signature.len, &sigc);
587+
sigc.payload_type = SIGNATURE_PAYLOAD_TAG;
588+
sigc.payload = strbuf_detach(&payload, &sigc.payload_len);
589+
status = check_signature(&sigc, signature.buf, signature.len);
588590
if (sigc.output)
589591
strbuf_addstr(&verify_message, sigc.output);
590592
else

t/lib-gpg.sh

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ test_lazy_prereq RFC1991 '
9090
GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key"
9191
GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key"
9292
GPGSSH_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key"
93+
GPGSSH_KEY_EXPIRED="${GNUPGHOME}/expired_ssh_signing_key"
94+
GPGSSH_KEY_NOTYETVALID="${GNUPGHOME}/notyetvalid_ssh_signing_key"
95+
GPGSSH_KEY_TIMEBOXEDVALID="${GNUPGHOME}/timeboxed_valid_ssh_signing_key"
96+
GPGSSH_KEY_TIMEBOXEDINVALID="${GNUPGHOME}/timeboxed_invalid_ssh_signing_key"
9397
GPGSSH_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key"
9498
GPGSSH_KEY_PASSPHRASE="super_secret"
9599
GPGSSH_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile"
@@ -105,21 +109,61 @@ test_lazy_prereq GPGSSH '
105109
echo $ssh_version | grep -q "find-principals:missing signature file"
106110
test $? = 0 || exit 1;
107111
108-
# some broken versions of ssh-keygen segfault on find-principals;
109-
# avoid testing with them.
110-
ssh-keygen -Y find-principals -f /dev/null -s /dev/null
111-
test $? = 139 && exit 1
112-
112+
# Setup some keys and an allowed signers file
113113
mkdir -p "${GNUPGHOME}" &&
114114
chmod 0700 "${GNUPGHOME}" &&
115115
(setfacl -k "${GNUPGHOME}" 2>/dev/null || true) &&
116116
ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null &&
117-
echo "\"principal with number 1\" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
118117
ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null &&
119-
echo "\"principal with number 2\" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
120118
ssh-keygen -t ed25519 -N "${GPGSSH_KEY_PASSPHRASE}" -C "git ed25519 encrypted key" -f "${GPGSSH_KEY_WITH_PASSPHRASE}" >/dev/null &&
121-
echo "\"principal with number 3\" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
122-
ssh-keygen -t ed25519 -N "" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null
119+
ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null &&
120+
121+
cat >"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF &&
122+
"principal with number 1" $(cat "${GPGSSH_KEY_PRIMARY}.pub")"
123+
"principal with number 2" $(cat "${GPGSSH_KEY_SECONDARY}.pub")"
124+
"principal with number 3" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")"
125+
EOF
126+
127+
# Verify if at least one key and ssh-keygen works as expected
128+
echo "testpayload" |
129+
ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_PRIMARY}" >gpgssh_prereq.sig &&
130+
ssh-keygen -Y find-principals -f "${GPGSSH_ALLOWED_SIGNERS}" -s gpgssh_prereq.sig &&
131+
echo "testpayload" |
132+
ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with number 1" -s gpgssh_prereq.sig
133+
'
134+
135+
test_lazy_prereq GPGSSH_VERIFYTIME '
136+
# Check if ssh-keygen has a verify-time option by passing an invalid date to it
137+
ssh-keygen -Overify-time=INVALID -Y check-novalidate -s doesnotmatter 2>&1 | grep -q -F "Invalid \"verify-time\"" &&
138+
139+
# Set up keys with key lifetimes
140+
ssh-keygen -t ed25519 -N "" -C "timeboxed valid key" -f "${GPGSSH_KEY_TIMEBOXEDVALID}" >/dev/null &&
141+
key_valid=$(cat "${GPGSSH_KEY_TIMEBOXEDVALID}.pub") &&
142+
ssh-keygen -t ed25519 -N "" -C "timeboxed invalid key" -f "${GPGSSH_KEY_TIMEBOXEDINVALID}" >/dev/null &&
143+
key_invalid=$(cat "${GPGSSH_KEY_TIMEBOXEDINVALID}.pub") &&
144+
ssh-keygen -t ed25519 -N "" -C "expired key" -f "${GPGSSH_KEY_EXPIRED}" >/dev/null &&
145+
key_expired=$(cat "${GPGSSH_KEY_EXPIRED}.pub") &&
146+
ssh-keygen -t ed25519 -N "" -C "not yet valid key" -f "${GPGSSH_KEY_NOTYETVALID}" >/dev/null &&
147+
key_notyetvalid=$(cat "${GPGSSH_KEY_NOTYETVALID}.pub") &&
148+
149+
# Timestamps outside of test_tick span
150+
ts2005a=20050401000000 ts2005b=200504020000 &&
151+
# Timestamps within test_tick span
152+
ts2005c=20050407000000 ts2005d=200504100000 &&
153+
# Definitely not yet valid / expired timestamps
154+
ts2000=20000101000000 ts2999=29990101000000 &&
155+
156+
cat >>"${GPGSSH_ALLOWED_SIGNERS}" <<-EOF &&
157+
"timeboxed valid key" valid-after="$ts2005c",valid-before="$ts2005d" $key_valid"
158+
"timeboxed invalid key" valid-after="$ts2005a",valid-before="$ts2005b" $key_invalid"
159+
"principal with expired key" valid-before="$ts2000" $key_expired"
160+
"principal with not yet valid key" valid-after="$ts2999" $key_notyetvalid"
161+
EOF
162+
163+
# and verify ssh-keygen verifies the key lifetime
164+
echo "testpayload" |
165+
ssh-keygen -Y sign -n "git" -f "${GPGSSH_KEY_EXPIRED}" >gpgssh_verifytime_prereq.sig &&
166+
! (ssh-keygen -Y verify -n "git" -f "${GPGSSH_ALLOWED_SIGNERS}" -I "principal with expired key" -s gpgssh_verifytime_prereq.sig)
123167
'
124168

125169
sanitize_pgp() {

0 commit comments

Comments
 (0)