Skip to content

Commit 29b3157

Browse files
FStelzergitster
authored andcommitted
ssh signing: add ssh key format and signing code
Implements the actual sign_buffer_ssh operation and move some shared cleanup code into a strbuf function Set gpg.format = ssh and user.signingkey to either a ssh public key string (like from an authorized_keys file), or a ssh key file. If the key file or the config value itself contains only a public key then the private key needs to be available via ssh-agent. gpg.ssh.program can be set to an alternative location of ssh-keygen. A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for this feature. Since only ssh-keygen is needed it can this way be installed seperately without upgrading your system openssh packages. Signed-off-by: Fabian Stelzer <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 64625c7 commit 29b3157

File tree

3 files changed

+137
-10
lines changed

3 files changed

+137
-10
lines changed

Documentation/config/gpg.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ gpg.program::
1111

1212
gpg.format::
1313
Specifies which key format to use when signing with `--gpg-sign`.
14-
Default is "openpgp" and another possible value is "x509".
14+
Default is "openpgp". Other possible values are "x509", "ssh".
1515

1616
gpg.<format>.program::
1717
Use this to customize the program used for the signing format you
1818
chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
1919
be used as a legacy synonym for `gpg.openpgp.program`. The default
20-
value for `gpg.x509.program` is "gpgsm".
20+
value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
2121

2222
gpg.minTrustLevel::
2323
Specifies a minimum trust level for signature verification. If

Documentation/config/user.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ user.signingKey::
3636
commit, you can override the default selection with this variable.
3737
This option is passed unchanged to gpg's --local-user parameter,
3838
so you may specify a key using any method that gpg supports.
39+
If gpg.format is set to "ssh" this can contain the literal ssh public
40+
key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and
41+
corresponds to the private key used for signing. The private key
42+
needs to be available via ssh-agent. Alternatively it can be set to
43+
a file containing a private key directly.

gpg-interface.c

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {
4141
NULL
4242
};
4343

44+
static const char *ssh_verify_args[] = { NULL };
45+
static const char *ssh_sigs[] = {
46+
"-----BEGIN SSH SIGNATURE-----",
47+
NULL
48+
};
49+
4450
static int verify_gpg_signed_buffer(struct signature_check *sigc,
4551
struct gpg_format *fmt, const char *payload,
4652
size_t payload_size, const char *signature,
4753
size_t signature_size);
4854
static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
4955
const char *signing_key);
56+
static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
57+
const char *signing_key);
5058

5159
static struct gpg_format gpg_format[] = {
5260
{
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {
6573
.verify_signed_buffer = verify_gpg_signed_buffer,
6674
.sign_buffer = sign_buffer_gpg,
6775
},
76+
{
77+
.name = "ssh",
78+
.program = "ssh-keygen",
79+
.verify_args = ssh_verify_args,
80+
.sigs = ssh_sigs,
81+
.verify_signed_buffer = NULL, /* TODO */
82+
.sign_buffer = sign_buffer_ssh
83+
},
6884
};
6985

7086
static struct gpg_format *use_format = &gpg_format[0];
@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)
443459
if (!strcmp(var, "gpg.x509.program"))
444460
fmtname = "x509";
445461

462+
if (!strcmp(var, "gpg.ssh.program"))
463+
fmtname = "ssh";
464+
446465
if (fmtname) {
447466
fmt = get_format_by_name(fmtname);
448467
return git_config_string(&fmt->program, var, value);
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
463482
return use_format->sign_buffer(buffer, signature, signing_key);
464483
}
465484

485+
/*
486+
* Strip CR from the line endings, in case we are on Windows.
487+
* NEEDSWORK: make it trim only CRs before LFs and rename
488+
*/
489+
static void remove_cr_after(struct strbuf *buffer, size_t offset)
490+
{
491+
size_t i, j;
492+
493+
for (i = j = offset; i < buffer->len; i++) {
494+
if (buffer->buf[i] != '\r') {
495+
if (i != j)
496+
buffer->buf[j] = buffer->buf[i];
497+
j++;
498+
}
499+
}
500+
strbuf_setlen(buffer, j);
501+
}
502+
466503
static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
467504
const char *signing_key)
468505
{
469506
struct child_process gpg = CHILD_PROCESS_INIT;
470507
int ret;
471-
size_t i, j, bottom;
508+
size_t bottom;
472509
struct strbuf gpg_status = STRBUF_INIT;
473510

474511
strvec_pushl(&gpg.args,
@@ -494,13 +531,98 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
494531
return error(_("gpg failed to sign the data"));
495532

496533
/* Strip CR from the line endings, in case we are on Windows. */
497-
for (i = j = bottom; i < signature->len; i++)
498-
if (signature->buf[i] != '\r') {
499-
if (i != j)
500-
signature->buf[j] = signature->buf[i];
501-
j++;
502-
}
503-
strbuf_setlen(signature, j);
534+
remove_cr_after(signature, bottom);
504535

505536
return 0;
506537
}
538+
539+
static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
540+
const char *signing_key)
541+
{
542+
struct child_process signer = CHILD_PROCESS_INIT;
543+
int ret = -1;
544+
size_t bottom, keylen;
545+
struct strbuf signer_stderr = STRBUF_INIT;
546+
struct tempfile *key_file = NULL, *buffer_file = NULL;
547+
char *ssh_signing_key_file = NULL;
548+
struct strbuf ssh_signature_filename = STRBUF_INIT;
549+
550+
if (!signing_key || signing_key[0] == '\0')
551+
return error(
552+
_("user.signingkey needs to be set for ssh signing"));
553+
554+
if (starts_with(signing_key, "ssh-")) {
555+
/* A literal ssh key */
556+
key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
557+
if (!key_file)
558+
return error_errno(
559+
_("could not create temporary file"));
560+
keylen = strlen(signing_key);
561+
if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
562+
close_tempfile_gently(key_file) < 0) {
563+
error_errno(_("failed writing ssh signing key to '%s'"),
564+
key_file->filename.buf);
565+
goto out;
566+
}
567+
ssh_signing_key_file = strbuf_detach(&key_file->filename, NULL);
568+
} else {
569+
/* We assume a file */
570+
ssh_signing_key_file = expand_user_path(signing_key, 1);
571+
}
572+
573+
buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
574+
if (!buffer_file) {
575+
error_errno(_("could not create temporary file"));
576+
goto out;
577+
}
578+
579+
if (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||
580+
close_tempfile_gently(buffer_file) < 0) {
581+
error_errno(_("failed writing ssh signing key buffer to '%s'"),
582+
buffer_file->filename.buf);
583+
goto out;
584+
}
585+
586+
strvec_pushl(&signer.args, use_format->program,
587+
"-Y", "sign",
588+
"-n", "git",
589+
"-f", ssh_signing_key_file,
590+
buffer_file->filename.buf,
591+
NULL);
592+
593+
sigchain_push(SIGPIPE, SIG_IGN);
594+
ret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);
595+
sigchain_pop(SIGPIPE);
596+
597+
if (ret) {
598+
if (strstr(signer_stderr.buf, "usage:"))
599+
error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
600+
601+
error("%s", signer_stderr.buf);
602+
goto out;
603+
}
604+
605+
bottom = signature->len;
606+
607+
strbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);
608+
strbuf_addstr(&ssh_signature_filename, ".sig");
609+
if (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {
610+
error_errno(
611+
_("failed reading ssh signing data buffer from '%s'"),
612+
ssh_signature_filename.buf);
613+
}
614+
unlink_or_warn(ssh_signature_filename.buf);
615+
616+
/* Strip CR from the line endings, in case we are on Windows. */
617+
remove_cr_after(signature, bottom);
618+
619+
out:
620+
if (key_file)
621+
delete_tempfile(&key_file);
622+
if (buffer_file)
623+
delete_tempfile(&buffer_file);
624+
strbuf_release(&signer_stderr);
625+
strbuf_release(&ssh_signature_filename);
626+
FREE_AND_NULL(ssh_signing_key_file);
627+
return ret;
628+
}

0 commit comments

Comments
 (0)