Skip to content

Commit facca53

Browse files
FStelzergitster
authored andcommitted
ssh signing: verify signatures using ssh-keygen
To verify a ssh signature we first call ssh-keygen -Y find-principal to look up the signing principal by their public key from the allowedSignersFile. If the key is found then we do a verify. Otherwise we only validate the signature but can not verify the signers identity. Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED SIGNERS") which contains valid public keys and a principal (usually user@domain). Depending on the environment this file can be managed by the individual developer or for example generated by the central repository server from known ssh keys with push access. This file is usually stored outside the repository, but if the repository only allows signed commits/pushes, the user might choose to store it in the repository. To revoke a key put the public key without the principal prefix into gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1) "KEY REVOCATION LISTS"). The same considerations about who to trust for verification as with the allowedSignersFile apply. Using SSH CA Keys with these files is also possible. Add "cert-authority" as key option between the principal and the key to mark it as a CA and all keys signed by it as valid for this CA. See "CERTIFICATES" in ssh-keygen(1). Signed-off-by: Fabian Stelzer <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 4838f62 commit facca53

File tree

3 files changed

+252
-2
lines changed

3 files changed

+252
-2
lines changed

Documentation/config/gpg.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,38 @@ gpg.ssh.defaultKeyCommand:
3939
signature is requested. On successful exit a valid ssh public key is
4040
expected in the first line of its output. To automatically use the first
4141
available key from your ssh-agent set this to "ssh-add -L".
42+
43+
gpg.ssh.allowedSignersFile::
44+
A file containing ssh public keys which you are willing to trust.
45+
The file consists of one or more lines of principals followed by an ssh
46+
public key.
47+
e.g.: [email protected],[email protected] ssh-rsa AAAAX1...
48+
See ssh-keygen(1) "ALLOWED SIGNERS" for details.
49+
The principal is only used to identify the key and is available when
50+
verifying a signature.
51+
+
52+
SSH has no concept of trust levels like gpg does. To be able to differentiate
53+
between valid signatures and trusted signatures the trust level of a signature
54+
verification is set to `fully` when the public key is present in the allowedSignersFile.
55+
Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.
56+
Otherwise valid but untrusted signatures will still verify but show no principal
57+
name of the signer.
58+
+
59+
This file can be set to a location outside of the repository and every developer
60+
maintains their own trust store. A central repository server could generate this
61+
file automatically from ssh keys with push access to verify the code against.
62+
In a corporate setting this file is probably generated at a global location
63+
from automation that already handles developer ssh keys.
64+
+
65+
A repository that only allows signed commits can store the file
66+
in the repository itself using a path relative to the top-level of the working tree.
67+
This way only committers with an already valid key can add or change keys in the keyring.
68+
+
69+
Using a SSH CA key with the cert-authority option
70+
(see ssh-keygen(1) "CERTIFICATES") is also valid.
71+
72+
gpg.ssh.revocationFile::
73+
Either a SSH KRL or a list of revoked public keys (without the principal prefix).
74+
See ssh-keygen(1) for details.
75+
If a public key is found in this file then it will always be treated
76+
as having trust level "never" and signatures will show as invalid.

builtin/receive-pack.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
131131
{
132132
int status = parse_hide_refs_config(var, value, "receive");
133133

134+
if (status)
135+
return status;
136+
137+
status = git_gpg_config(var, value, NULL);
134138
if (status)
135139
return status;
136140

gpg-interface.c

Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
#include "config.h"
44
#include "run-command.h"
55
#include "strbuf.h"
6+
#include "dir.h"
67
#include "gpg-interface.h"
78
#include "sigchain.h"
89
#include "tempfile.h"
910
#include "alias.h"
1011

1112
static char *configured_signing_key;
12-
static const char *ssh_default_key_command;
13+
static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;
1314
static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
1415

1516
struct gpg_format {
@@ -55,6 +56,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
5556
struct gpg_format *fmt, const char *payload,
5657
size_t payload_size, const char *signature,
5758
size_t signature_size);
59+
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,
62+
size_t signature_size);
5863
static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
5964
const char *signing_key);
6065
static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
@@ -90,7 +95,7 @@ static struct gpg_format gpg_format[] = {
9095
.program = "ssh-keygen",
9196
.verify_args = ssh_verify_args,
9297
.sigs = ssh_sigs,
93-
.verify_signed_buffer = NULL, /* TODO */
98+
.verify_signed_buffer = verify_ssh_signed_buffer,
9499
.sign_buffer = sign_buffer_ssh,
95100
.get_default_key = get_default_ssh_signing_key,
96101
.get_key_id = get_ssh_key_id,
@@ -357,6 +362,200 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
357362
return ret;
358363
}
359364

365+
static void parse_ssh_output(struct signature_check *sigc)
366+
{
367+
const char *line, *principal, *search;
368+
char *key = NULL;
369+
370+
/*
371+
* ssh-keygen output should be:
372+
* Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
373+
*
374+
* or for valid but unknown keys:
375+
* Good "git" signature with RSA key SHA256:FINGERPRINT
376+
*
377+
* Note that "PRINCIPAL" can contain whitespace, "RSA" and
378+
* "SHA256" part could be a different token that names of
379+
* the algorithms used, and "FINGERPRINT" is a hexadecimal
380+
* string. By finding the last occurence of " with ", we can
381+
* reliably parse out the PRINCIPAL.
382+
*/
383+
sigc->result = 'B';
384+
sigc->trust_level = TRUST_NEVER;
385+
386+
line = xmemdupz(sigc->output, strcspn(sigc->output, "\n"));
387+
388+
if (skip_prefix(line, "Good \"git\" signature for ", &line)) {
389+
/* Valid signature and known principal */
390+
sigc->result = 'G';
391+
sigc->trust_level = TRUST_FULLY;
392+
393+
/* Search for the last "with" to get the full principal */
394+
principal = line;
395+
do {
396+
search = strstr(line, " with ");
397+
if (search)
398+
line = search + 1;
399+
} while (search != NULL);
400+
sigc->signer = xmemdupz(principal, line - principal - 1);
401+
} else if (skip_prefix(line, "Good \"git\" signature with ", &line)) {
402+
/* Valid signature, but key unknown */
403+
sigc->result = 'G';
404+
sigc->trust_level = TRUST_UNDEFINED;
405+
} else {
406+
return;
407+
}
408+
409+
key = strstr(line, "key");
410+
if (key) {
411+
sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
412+
sigc->key = xstrdup(sigc->fingerprint);
413+
} else {
414+
/*
415+
* Output did not match what we expected
416+
* Treat the signature as bad
417+
*/
418+
sigc->result = 'B';
419+
}
420+
}
421+
422+
static int verify_ssh_signed_buffer(struct signature_check *sigc,
423+
struct gpg_format *fmt, const char *payload,
424+
size_t payload_size, const char *signature,
425+
size_t signature_size)
426+
{
427+
struct child_process ssh_keygen = CHILD_PROCESS_INIT;
428+
struct tempfile *buffer_file;
429+
int ret = -1;
430+
const char *line;
431+
size_t trust_size;
432+
char *principal;
433+
struct strbuf ssh_principals_out = STRBUF_INIT;
434+
struct strbuf ssh_principals_err = STRBUF_INIT;
435+
struct strbuf ssh_keygen_out = STRBUF_INIT;
436+
struct strbuf ssh_keygen_err = STRBUF_INIT;
437+
438+
if (!ssh_allowed_signers) {
439+
error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification"));
440+
return -1;
441+
}
442+
443+
buffer_file = mks_tempfile_t(".git_vtag_tmpXXXXXX");
444+
if (!buffer_file)
445+
return error_errno(_("could not create temporary file"));
446+
if (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||
447+
close_tempfile_gently(buffer_file) < 0) {
448+
error_errno(_("failed writing detached signature to '%s'"),
449+
buffer_file->filename.buf);
450+
delete_tempfile(&buffer_file);
451+
return -1;
452+
}
453+
454+
/* Find the principal from the signers */
455+
strvec_pushl(&ssh_keygen.args, fmt->program,
456+
"-Y", "find-principals",
457+
"-f", ssh_allowed_signers,
458+
"-s", buffer_file->filename.buf,
459+
NULL);
460+
ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0,
461+
&ssh_principals_err, 0);
462+
if (ret && strstr(ssh_principals_err.buf, "usage:")) {
463+
error(_("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)"));
464+
goto out;
465+
}
466+
if (ret || !ssh_principals_out.len) {
467+
/*
468+
* We did not find a matching principal in the allowedSigners
469+
* Check without validation
470+
*/
471+
child_process_init(&ssh_keygen);
472+
strvec_pushl(&ssh_keygen.args, fmt->program,
473+
"-Y", "check-novalidate",
474+
"-n", "git",
475+
"-s", buffer_file->filename.buf,
476+
NULL);
477+
pipe_command(&ssh_keygen, payload, payload_size,
478+
&ssh_keygen_out, 0, &ssh_keygen_err, 0);
479+
480+
/*
481+
* Fail on unknown keys
482+
* we still call check-novalidate to display the signature info
483+
*/
484+
ret = -1;
485+
} else {
486+
/* Check every principal we found (one per line) */
487+
for (line = ssh_principals_out.buf; *line;
488+
line = strchrnul(line + 1, '\n')) {
489+
while (*line == '\n')
490+
line++;
491+
if (!*line)
492+
break;
493+
494+
trust_size = strcspn(line, "\n");
495+
principal = xmemdupz(line, trust_size);
496+
497+
child_process_init(&ssh_keygen);
498+
strbuf_release(&ssh_keygen_out);
499+
strbuf_release(&ssh_keygen_err);
500+
strvec_push(&ssh_keygen.args, fmt->program);
501+
/*
502+
* We found principals
503+
* Try with each until we find a match
504+
*/
505+
strvec_pushl(&ssh_keygen.args, "-Y", "verify",
506+
"-n", "git",
507+
"-f", ssh_allowed_signers,
508+
"-I", principal,
509+
"-s", buffer_file->filename.buf,
510+
NULL);
511+
512+
if (ssh_revocation_file) {
513+
if (file_exists(ssh_revocation_file)) {
514+
strvec_pushl(&ssh_keygen.args, "-r",
515+
ssh_revocation_file, NULL);
516+
} else {
517+
warning(_("ssh signing revocation file configured but not found: %s"),
518+
ssh_revocation_file);
519+
}
520+
}
521+
522+
sigchain_push(SIGPIPE, SIG_IGN);
523+
ret = pipe_command(&ssh_keygen, payload, payload_size,
524+
&ssh_keygen_out, 0, &ssh_keygen_err, 0);
525+
sigchain_pop(SIGPIPE);
526+
527+
FREE_AND_NULL(principal);
528+
529+
if (!ret)
530+
ret = !starts_with(ssh_keygen_out.buf, "Good");
531+
532+
if (!ret)
533+
break;
534+
}
535+
}
536+
537+
sigc->payload = xmemdupz(payload, payload_size);
538+
strbuf_stripspace(&ssh_keygen_out, 0);
539+
strbuf_stripspace(&ssh_keygen_err, 0);
540+
/* Add stderr outputs to show the user actual ssh-keygen errors */
541+
strbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);
542+
strbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);
543+
sigc->output = strbuf_detach(&ssh_keygen_out, NULL);
544+
sigc->gpg_status = xstrdup(sigc->output);
545+
546+
parse_ssh_output(sigc);
547+
548+
out:
549+
if (buffer_file)
550+
delete_tempfile(&buffer_file);
551+
strbuf_release(&ssh_principals_out);
552+
strbuf_release(&ssh_principals_err);
553+
strbuf_release(&ssh_keygen_out);
554+
strbuf_release(&ssh_keygen_err);
555+
556+
return ret;
557+
}
558+
360559
int check_signature(const char *payload, size_t plen, const char *signature,
361560
size_t slen, struct signature_check *sigc)
362561
{
@@ -473,6 +672,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)
473672
return git_config_string(&ssh_default_key_command, var, value);
474673
}
475674

675+
if (!strcmp(var, "gpg.ssh.allowedsignersfile")) {
676+
if (!value)
677+
return config_error_nonbool(var);
678+
return git_config_pathname(&ssh_allowed_signers, var, value);
679+
}
680+
681+
if (!strcmp(var, "gpg.ssh.revocationfile")) {
682+
if (!value)
683+
return config_error_nonbool(var);
684+
return git_config_pathname(&ssh_revocation_file, var, value);
685+
}
686+
476687
if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
477688
fmtname = "openpgp";
478689

0 commit comments

Comments
 (0)