]> git.ipfire.org Git - thirdparty/git.git/commitdiff
ssh signing: add ssh key format and signing code
authorFabian Stelzer <fs@gigacodes.de>
Fri, 10 Sep 2021 20:07:36 +0000 (20:07 +0000)
committerJunio C Hamano <gitster@pobox.com>
Fri, 10 Sep 2021 21:15:51 +0000 (14:15 -0700)
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>
Documentation/config/gpg.txt
Documentation/config/user.txt
gpg-interface.c

index d94025cb3684d8e20f2875716cc16dfad47cf6ff..88531b15f0ff324f88a8e44857a1ce945dbeaae2 100644 (file)
@@ -11,13 +11,13 @@ gpg.program::
 
 gpg.format::
        Specifies which key format to use when signing with `--gpg-sign`.
-       Default is "openpgp" and another possible value is "x509".
+       Default is "openpgp". Other possible values are "x509", "ssh".
 
 gpg.<format>.program::
        Use this to customize the program used for the signing format you
        chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
        be used as a legacy synonym for `gpg.openpgp.program`. The default
-       value for `gpg.x509.program` is "gpgsm".
+       value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
 
 gpg.minTrustLevel::
        Specifies a minimum trust level for signature verification.  If
index 59aec7c3aed32aac0a8d7e015c0602705e4ccc6d..2155128957c93ea9e43b36c64bae635db75dfe23 100644 (file)
@@ -36,3 +36,8 @@ user.signingKey::
        commit, you can override the default selection with this variable.
        This option is passed unchanged to gpg's --local-user parameter,
        so you may specify a key using any method that gpg supports.
+       If gpg.format is set to "ssh" this can contain the literal ssh public
+       key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and
+       corresponds to the private key used for signing. The private key
+       needs to be available via ssh-agent. Alternatively it can be set to
+       a file containing a private key directly.
index db54b05416257d1411119439ff71ad2dce9917ab..7ca682ac6d68495a01bd1d76ed49bcc1adab2245 100644 (file)
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {
        NULL
 };
 
+static const char *ssh_verify_args[] = { NULL };
+static const char *ssh_sigs[] = {
+       "-----BEGIN SSH SIGNATURE-----",
+       NULL
+};
+
 static int verify_gpg_signed_buffer(struct signature_check *sigc,
                                    struct gpg_format *fmt, const char *payload,
                                    size_t payload_size, const char *signature,
                                    size_t signature_size);
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
                           const char *signing_key);
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+                          const char *signing_key);
 
 static struct gpg_format gpg_format[] = {
        {
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {
                .verify_signed_buffer = verify_gpg_signed_buffer,
                .sign_buffer = sign_buffer_gpg,
        },
+       {
+               .name = "ssh",
+               .program = "ssh-keygen",
+               .verify_args = ssh_verify_args,
+               .sigs = ssh_sigs,
+               .verify_signed_buffer = NULL, /* TODO */
+               .sign_buffer = sign_buffer_ssh
+       },
 };
 
 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)
        if (!strcmp(var, "gpg.x509.program"))
                fmtname = "x509";
 
+       if (!strcmp(var, "gpg.ssh.program"))
+               fmtname = "ssh";
+
        if (fmtname) {
                fmt = get_format_by_name(fmtname);
                return git_config_string(&fmt->program, var, value);
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
        return use_format->sign_buffer(buffer, signature, signing_key);
 }
 
+/*
+ * Strip CR from the line endings, in case we are on Windows.
+ * NEEDSWORK: make it trim only CRs before LFs and rename
+ */
+static void remove_cr_after(struct strbuf *buffer, size_t offset)
+{
+       size_t i, j;
+
+       for (i = j = offset; i < buffer->len; i++) {
+               if (buffer->buf[i] != '\r') {
+                       if (i != j)
+                               buffer->buf[j] = buffer->buf[i];
+                       j++;
+               }
+       }
+       strbuf_setlen(buffer, j);
+}
+
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
                          const char *signing_key)
 {
        struct child_process gpg = CHILD_PROCESS_INIT;
        int ret;
-       size_t i, j, bottom;
+       size_t bottom;
        struct strbuf gpg_status = STRBUF_INIT;
 
        strvec_pushl(&gpg.args,
@@ -494,13 +531,98 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
                return error(_("gpg failed to sign the data"));
 
        /* Strip CR from the line endings, in case we are on Windows. */
-       for (i = j = bottom; i < signature->len; i++)
-               if (signature->buf[i] != '\r') {
-                       if (i != j)
-                               signature->buf[j] = signature->buf[i];
-                       j++;
-               }
-       strbuf_setlen(signature, j);
+       remove_cr_after(signature, bottom);
 
        return 0;
 }
+
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+                          const char *signing_key)
+{
+       struct child_process signer = CHILD_PROCESS_INIT;
+       int ret = -1;
+       size_t bottom, keylen;
+       struct strbuf signer_stderr = STRBUF_INIT;
+       struct tempfile *key_file = NULL, *buffer_file = NULL;
+       char *ssh_signing_key_file = NULL;
+       struct strbuf ssh_signature_filename = STRBUF_INIT;
+
+       if (!signing_key || signing_key[0] == '\0')
+               return error(
+                       _("user.signingkey needs to be set for ssh signing"));
+
+       if (starts_with(signing_key, "ssh-")) {
+               /* A literal ssh key */
+               key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
+               if (!key_file)
+                       return error_errno(
+                               _("could not create temporary file"));
+               keylen = strlen(signing_key);
+               if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
+                   close_tempfile_gently(key_file) < 0) {
+                       error_errno(_("failed writing ssh signing key to '%s'"),
+                                   key_file->filename.buf);
+                       goto out;
+               }
+               ssh_signing_key_file = strbuf_detach(&key_file->filename, NULL);
+       } else {
+               /* We assume a file */
+               ssh_signing_key_file = expand_user_path(signing_key, 1);
+       }
+
+       buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
+       if (!buffer_file) {
+               error_errno(_("could not create temporary file"));
+               goto out;
+       }
+
+       if (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||
+           close_tempfile_gently(buffer_file) < 0) {
+               error_errno(_("failed writing ssh signing key buffer to '%s'"),
+                           buffer_file->filename.buf);
+               goto out;
+       }
+
+       strvec_pushl(&signer.args, use_format->program,
+                    "-Y", "sign",
+                    "-n", "git",
+                    "-f", ssh_signing_key_file,
+                    buffer_file->filename.buf,
+                    NULL);
+
+       sigchain_push(SIGPIPE, SIG_IGN);
+       ret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);
+       sigchain_pop(SIGPIPE);
+
+       if (ret) {
+               if (strstr(signer_stderr.buf, "usage:"))
+                       error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
+
+               error("%s", signer_stderr.buf);
+               goto out;
+       }
+
+       bottom = signature->len;
+
+       strbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);
+       strbuf_addstr(&ssh_signature_filename, ".sig");
+       if (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {
+               error_errno(
+                       _("failed reading ssh signing data buffer from '%s'"),
+                       ssh_signature_filename.buf);
+       }
+       unlink_or_warn(ssh_signature_filename.buf);
+
+       /* Strip CR from the line endings, in case we are on Windows. */
+       remove_cr_after(signature, bottom);
+
+out:
+       if (key_file)
+               delete_tempfile(&key_file);
+       if (buffer_file)
+               delete_tempfile(&buffer_file);
+       strbuf_release(&signer_stderr);
+       strbuf_release(&ssh_signature_filename);
+       FREE_AND_NULL(ssh_signing_key_file);
+       return ret;
+}