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 <fs@gigacodes.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Fabian Stelzer committed Sep 10, 2021 at 20:07 UTC 29b315778e958417a411f02b6d4b5a0fc9d731e2
3 files changed +137 -10
Documentation/config/gpg.txt
+2 -2
@@ -11,13 +11,13 @@ gpg.program::
11
12 gpg.format::
13 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".
15
16 gpg.<format>.program::
17 Use this to customize the program used for the signing format you
18 chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
19 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".
21
22 gpg.minTrustLevel::
23 Specifies a minimum trust level for signature verification. If
Documentation/config/user.txt
+5
@@ -36,3 +36,8 @@ user.signingKey::
36 commit, you can override the default selection with this variable.
37 This option is passed unchanged to gpg's --local-user parameter,
38 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
+130 -8
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {
41 NULL
42 };
43
44 +static const char *ssh_verify_args[] = { NULL };
45 +static const char *ssh_sigs[] = {
46 + "-----BEGIN SSH SIGNATURE-----",
47 + NULL
48 +};
49 +
50 static int verify_gpg_signed_buffer(struct signature_check *sigc,
51 struct gpg_format *fmt, const char *payload,
52 size_t payload_size, const char *signature,
53 size_t signature_size);
54 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
55 const char *signing_key);
56 +static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
57 + const char *signing_key);
58
59 static struct gpg_format gpg_format[] = {
60 {
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {
73 .verify_signed_buffer = verify_gpg_signed_buffer,
74 .sign_buffer = sign_buffer_gpg,
75 },
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 + },
84 };
85
86 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)
459 if (!strcmp(var, "gpg.x509.program"))
460 fmtname = "x509";
461
462 + if (!strcmp(var, "gpg.ssh.program"))
463 + fmtname = "ssh";
464 +
465 if (fmtname) {
466 fmt = get_format_by_name(fmtname);
467 return git_config_string(&fmt->program, var, value);
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
482 return use_format->sign_buffer(buffer, signature, signing_key);
483 }
484
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 +
503 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
504 const char *signing_key)
505 {
506 struct child_process gpg = CHILD_PROCESS_INIT;
507 int ret;
471 - size_t i, j, bottom;
508 + size_t bottom;
509 struct strbuf gpg_status = STRBUF_INIT;
510
511 strvec_pushl(&gpg.args,
@@ -494,13 +531,98 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
531 return error(_("gpg failed to sign the data"));
532
533 /* 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);
535
536 return 0;
537 }
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 +}