{"thread":{"id":"56054","subject":"[PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","startedAt":"2021-07-06T08:19:59Z","lastAt":"2021-12-30T11:10:50Z","messageCount":153,"participants":["Fabian Stelzer via GitGitGadget","Han-Wen Nienhuys","Fabian Stelzer","brian m. carlson","Junio C Hamano","Randall S. Becker","Bagas Sanjaya","Ævar Arnfjörð Bjarmason","Felipe Contreras","Eric Sunshine","Gwyneth Morgan","Jonathan Tan","Josh Steadmon","Carlo Arenas"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"429279","messageId":"pull.1041.git.git.1625559593910.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":null,"subject":"[PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-06T08:19:53Z","receivedAt":"2021-07-06T08:19:59Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nset gpg.format = ssh and user.signingkey to a ssh public key string (like from an\nauthorized_keys file) and commits/tags can be signed using the private\nkey from your ssh-agent.\n\nVerification uses a allowed_signers_file (see ssh-keygen(1)) which\ndefaults to .gitsigners but can be set via gpg.ssh.allowedsigners\nA possible gpg.ssh.revocationfile is also passed to ssh-keygen on\nverification.\n\nneeds openssh>8.2p1\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n    RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen\n    \n    Support for using private keyfiles directly is still missing and i'm\n    unsure on how to configure it or if the pubkey in the signingkey field\n    is such a good idea. A SSH Fingerprint as signingkey would be nicer, but\n    key lookup would be quite cumbersome. Maybe storing the fingerprint in\n    signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile\n    setting? As a default we could get the first ssh key from ssh-add and\n    store it in the config to avoid unintentional changes of the used\n    signing key. I've started with some tests for SSH Signing but having\n    static private keyfiles would make this a lot easier. So still on my\n    TODO.\n    \n    This feature makes git signing much more accessible to the average user.\n    Usually they have a SSH Key for pushing code already. Using it for\n    signing commits allows us to verify not only the transport but the\n    pushed code as well. The allowed_signers file could be kept in the\n    repository if all receives are verified (allowing only useris with valid\n    signatures to add/change them) or outside if generated/managed\n    differently. Tools like gitolite could optionally generate and enforce\n    them from the already existing user ssh keys for example.\n    \n    In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n    signing/encryption and ssh keys which i think is quite common (at least\n    for the email part). This way we can establish the correct trust for the\n    SSH Keys without setting up a separate GPG Infrastructure (which is\n    still quite painful for users) or implementing x509 signing support for\n    git (which lacks good forwarding mechanisms). Using ssh agent forwarding\n    makes this feature easily usable in todays development environments\n    where code is often checked out in remote VMs / containers.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v1\nPull-Request: https://github.com/git/git/pull/1041\n\n Documentation/config/gpg.txt  |  13 ++-\n Documentation/config/user.txt |   4 +\n gpg-interface.c               | 212 ++++++++++++++++++++++++++++++----\n gpg-interface.h               |   3 +\n 4 files changed, 205 insertions(+), 27 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..fd71bd782ec 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\n@@ -27,6 +27,15 @@ gpg.minTrustLevel::\n \twith at least `undefined` trust.  Setting this option overrides\n \tthe required trust-level for all operations.  Supported values,\n \tin increasing order of significance:\n+\n+gpg.ssh.allowedSigners::\n+\tA file containing all valid SSH signing principals. \n+\tSimilar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.\n+\tDefaults to .gitsigners\n+\n+gpg.ssh.revocationFile::\n+\tEither a SSH KRL or a list of revoked public keys.\n+\tSee ssh-keygen(1) for details.\n +\n * `undefined`\n * `never`\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..1632e7b320f 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,7 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this needs to contain the valid\n+\tssh public key (e.g.: \"ssh-rsa XXXXXX identifier\") which corresponds\n+\tto the private key used for signing. The private key needs to be available\n+\tvia ssh-agent. Direct private key files are not supported yet.\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..53504f64410 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -8,6 +8,7 @@\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+const char *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -35,6 +36,14 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = {\n+\tNULL\n+};\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static struct gpg_format gpg_format[] = {\n \t{ .name = \"openpgp\", .program = \"gpg\",\n \t  .verify_args = openpgp_verify_args,\n@@ -44,6 +53,9 @@ static struct gpg_format gpg_format[] = {\n \t  .verify_args = x509_verify_args,\n \t  .sigs = x509_sigs\n \t},\n+\t{ .name = \"ssh\", .program = \"ssh-keygen\",\n+\t  .verify_args = ssh_verify_args,\n+\t  .sigs = ssh_sigs },\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -144,6 +156,38 @@ static int parse_gpg_trust_level(const char *level,\n \treturn 1;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tconst char *output = NULL;\n+\tchar *next = NULL;\n+\n+\t// ssh-keysign output should be:\n+\t// Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\n+\toutput = xmemdupz(sigc->gpg_status, strcspn(sigc->gpg_status, \" \\n\"));\n+\tif (skip_prefix(sigc->gpg_status, \"Good \\\"git\\\" signature for \", &output)) {\n+\t\tsigc->result = 'G';\n+\n+\t\tnext = strchrnul(output, ' ');\n+\t\treplace_cstring(&sigc->signer, output, next);\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // 'with'\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // KEY Type\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // 'key'\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // key\n+\t\treplace_cstring(&sigc->fingerprint, output, next);\n+\t} else {\n+\t\tsigc->result = 'B';\n+\t}\n+\n+\t// SSH-Keygen prints onto stdout instead of stderr like the output code expects - so we just copy it over\n+\tfree(sigc->gpg_output);\n+\tsigc->gpg_output = xmemdupz(sigc->gpg_status, strlen(sigc->gpg_status));\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n@@ -262,11 +306,17 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\t\t\tstruct strbuf *gpg_output,\n \t\t\t\tstruct strbuf *gpg_status)\n {\n-\tstruct child_process gpg = CHILD_PROCESS_INIT;\n+\tstruct child_process gpg = CHILD_PROCESS_INIT,\n+\t\t\t     ssh_keygen = CHILD_PROCESS_INIT;\n \tstruct gpg_format *fmt;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf buf = STRBUF_INIT,\n+\t\t      principal_out = STRBUF_INIT,\n+\t\t      principal_err = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -283,24 +333,77 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \tif (!fmt)\n \t\tBUG(\"bad signature '%s'\", signature);\n \n-\tstrvec_push(&gpg.args, fmt->program);\n-\tstrvec_pushv(&gpg.args, fmt->verify_args);\n-\tstrvec_pushl(&gpg.args,\n-\t\t     \"--status-fd=1\",\n-\t\t     \"--verify\", temp->filename.buf, \"-\",\n-\t\t     NULL);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\t// Find the principal from the  signers\n+\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n+\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"find-principals\",\n+\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n+\t\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\t\tNULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &principal_out, 0, &principal_err, 0);\n+\t\tif (strstr(principal_err.buf, \"unknown option\")) {\n+\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n+\t\t}\n+\t\tif (ret || !principal_out.len)\n+\t\t\tgoto out;\n+\n+\t\t/* Iterate over all lines */\n+\t\tfor (line = principal_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n \n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n+\t\t\ttrust_size = strcspn(line, \" \\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n \n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n-\tsigchain_pop(SIGPIPE);\n+\t\t\tstrvec_push(&gpg.args,fmt->program);\n+\t\t\t// We found principals - Try with each until we find a match\n+\t\t\tstrvec_pushl(&gpg.args, \"-Y\", \"verify\",\n+\t\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n+\t\t\t\t\t\t\"-I\", principal,\n+\t\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\t\t NULL);\n \n-\tdelete_tempfile(&temp);\n+\t\t\tif (ssh_revocation_file) {\n+\t\t\t\tstrvec_pushl(&gpg.args, \"-r\", ssh_revocation_file, NULL);\n+\t\t\t}\n+\n+\t\t\tif (!gpg_status)\n+\t\t\t\tgpg_status = &buf;\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&gpg, payload, payload_size,\n+\t\t\t\t\t   gpg_status, 0, gpg_output, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n+\t\t\tret |= !strstr(gpg_status->buf, \"Good\");\n+\t\t\tif (ret == 0)\n+\t\t\t\tbreak;\n+\t\t}\n+\t} else {\n+\t\tstrvec_push(&gpg.args, fmt->program);\n+\t\tstrvec_pushv(&gpg.args, fmt->verify_args);\n+\t\tstrvec_pushl(&gpg.args,\n+\t\t\t\t\"--status-fd=1\",\n+\t\t\t\t\"--verify\", temp->filename.buf, \"-\",\n+\t\t\t\tNULL);\n+\n+\t\tif (!gpg_status)\n+\t\t\tgpg_status = &buf;\n+\n+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\tret = pipe_command(&gpg, payload, payload_size, gpg_status, 0,\n+\t\t\t\t   gpg_output, 0);\n+\t\tsigchain_pop(SIGPIPE);\n+\t\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n+\t}\n+\n+out:\n+\tdelete_tempfile(&temp);\n+\tstrbuf_release(&principal_out);\n+\tstrbuf_release(&principal_err);\n \tstrbuf_release(&buf); /* no matter it was used or not */\n \n \treturn ret;\n@@ -323,7 +426,11 @@ int check_signature(const char *payload, size_t plen, const char *signature,\n \tsigc->payload = xmemdupz(payload, plen);\n \tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n \tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\tparse_ssh_output(sigc);\n+\t} else {\n+\t\tparse_gpg_output(sigc);\n+\t}\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n@@ -394,6 +501,14 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.allowedsigners\")) {\n+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationfile\")) {\n+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.format\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n@@ -425,6 +540,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -437,7 +555,19 @@ const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\t// We could simply use the first key listed by ssh-add -L and risk signing with the wrong key\n+\t\treturn \"\";\n+\t} else {\n+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n+\t}\n+}\n+\n+const char *get_ssh_allowed_signers(void)\n+{\n+\tif (ssh_allowed_signers)\n+\t\treturn ssh_allowed_signers;\n+\treturn GPG_SSH_ALLOWED_SIGNERS;\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n@@ -446,12 +576,35 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \tint ret;\n \tsize_t i, j, bottom;\n \tstruct strbuf gpg_status = STRBUF_INIT;\n-\n-\tstrvec_pushl(&gpg.args,\n-\t\t     use_format->program,\n-\t\t     \"--status-fd=2\",\n-\t\t     \"-bsau\", signing_key,\n-\t\t     NULL);\n+\tstruct tempfile *temp = NULL;\n+\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\tif (!signing_key)\n+\t\t\treturn error(_(\"user.signingkey needs to be set to a ssh public key for ssh signing\"));\n+\n+\t\t// signing_key is a public ssh key\n+\t\t// FIXME: Allow specifying a key file so we can use private keyfiles instead of ssh-agent\n+\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\tif (!temp)\n+\t\t\treturn error_errno(_(\"could not create temporary file\"));\n+\t\tif (write_in_full(temp->fd, signing_key,\n+\t\t\t\t\tstrlen(signing_key)) < 0 ||\n+\t\t\tclose_tempfile_gently(temp) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"), temp->filename.buf);\n+\t\t\tdelete_tempfile(&temp);\n+\t\t\treturn -1;\n+\t\t}\n+\t\tstrvec_pushl(&gpg.args, use_format->program ,\n+\t\t\t\t\t\"-Y\", \"sign\",\n+\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\"-f\", temp->filename.buf,\n+\t\t\t\t\tNULL);\n+\t} else {\n+\t\tstrvec_pushl(&gpg.args, use_format->program ,\n+\t\t\t\t\t\"--status-fd=2\",\n+\t\t\t\t\t\"-bsau\", signing_key,\n+\t\t\t\t\tNULL);\n+\t}\n \n \tbottom = signature->len;\n \n@@ -464,7 +617,16 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \t\t\t   signature, 1024, &gpg_status, 0);\n \tsigchain_pop(SIGPIPE);\n \n-\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n+\tif (temp)\n+\t\tdelete_tempfile(&temp);\n+\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\tif (strstr(gpg_status.buf, \"unknown option\")) {\n+\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n+\t\t}\n+\t} else {\n+\t\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n+\t}\n \tstrbuf_release(&gpg_status);\n \tif (ret)\n \t\treturn error(_(\"gpg failed to sign the data\"));\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..286c1b4167a 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -7,6 +7,8 @@ struct strbuf;\n #define GPG_VERIFY_RAW\t\t\t2\n #define GPG_VERIFY_OMIT_STATUS\t4\n \n+#define GPG_SSH_ALLOWED_SIGNERS \".gitsigners\"\n+\n enum signature_trust_level {\n \tTRUST_UNDEFINED,\n \tTRUST_NEVER,\n@@ -64,6 +66,7 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+const char *get_ssh_allowed_signers(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\n\nbase-commit: 670b81a890388c60b7032a4f5b879f2ece8c4558\n-- \ngitgitgadget\n"},{"id":"429281","messageId":"CAFQ2z_O8fQ4eaCL30jpe8-_q5Cez4jH_E3v5WbgZ0ZgVZYZcyQ@mail.gmail.com","threadId":"56054","inReplyTo":"pull.1041.git.git.1625559593910.gitgitgadget@gmail.com","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Han-Wen Nienhuys","fromEmail":"hanwen@google.com","sentAt":"2021-07-06T10:07:23Z","receivedAt":"2021-07-06T10:07:36Z","isPatch":true,"sender":{"key":"hanwen@google.com","avatar":"https://avatars.githubusercontent.com/u/31547?v=4"},"body":"On Tue, Jul 6, 2021 at 10:20 AM Fabian Stelzer via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n> authorized_keys file) and commits/tags can be signed using the private\n> key from your ssh-agent.\n>\n> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n> verification.\n>\n...\n>     In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n>     signing/encryption and ssh keys which i think is quite common (at least\n>     for the email part). This way we can establish the correct trust for the\n>     SSH Keys without setting up a separate GPG Infrastructure (which is\n>     still quite painful for users) or implementing x509 signing support for\n>     git (which lacks good forwarding mechanisms). Using ssh agent forwarding\n>     makes this feature easily usable in todays development environments\n>     where code is often checked out in remote VMs / containers.\n\nThanks for working on this, and I support this initiative. I\ncoincidentally have started proselytizing something similar just weeks\nago.\n\nMy interest is in signing pushes rather than commits/tags, as that (in\ncombination with SSH U2F support) provides a simple mechanism to\nrequire (forwardable!) 2-factor authentication on pushes over HTTP. I\nhaven't looked at the signing code in detail, but I had the impression\nthat adding SSH signatures would automatically also add support for\nsigned pushes? (aka. push-certs) Do you know?\n\n-- \nHan-Wen Nienhuys - Google Munich\nI work 80%. Don't expect answers from me on Fridays.\n--\n\nGoogle Germany GmbH, Erika-Mann-Strasse 33, 80636 Munich\n\nRegistergericht und -nummer: Hamburg, HRB 86891\n\nSitz der Gesellschaft: Hamburg\n\nGeschäftsführer: Paul Manicle, Halimah DeLaine Prado\n"},{"id":"429282","messageId":"b5a5cf87-afca-aaa9-b309-da702b3b18de@gigacodes.de","threadId":"56054","inReplyTo":"CAFQ2z_O8fQ4eaCL30jpe8-_q5Cez4jH_E3v5WbgZ0ZgVZYZcyQ@mail.gmail.com","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-06T11:23:33Z","receivedAt":"2021-07-06T11:34:43Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":">> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n>> authorized_keys file) and commits/tags can be signed using the private\n>> key from your ssh-agent.\n>>\n>> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n>> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n>> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n>> verification.\n>>\n> ...\n>>      In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n>>      signing/encryption and ssh keys which i think is quite common (at least\n>>      for the email part). This way we can establish the correct trust for the\n>>      SSH Keys without setting up a separate GPG Infrastructure (which is\n>>      still quite painful for users) or implementing x509 signing support for\n>>      git (which lacks good forwarding mechanisms). Using ssh agent forwarding\n>>      makes this feature easily usable in todays development environments\n>>      where code is often checked out in remote VMs / containers.\n> Thanks for working on this, and I support this initiative. I\n> coincidentally have started proselytizing something similar just weeks\n> ago.\n>\n> My interest is in signing pushes rather than commits/tags, as that (in\n> combination with SSH U2F support) provides a simple mechanism to\n> require (forwardable!) 2-factor authentication on pushes over HTTP. I\n> haven't looked at the signing code in detail, but I had the impression\n> that adding SSH signatures would automatically also add support for\n> signed pushes? (aka. push-certs) Do you know?\n>\nUp until now i was not actually aware of the \"push signing\" \nfunctionality in git.\nI can see that the send/receive-pack use the same api function calls as \ncommit/tag signing.\nSo this should work just as well. Especially if using an ssh agent the whole\nprocess is identical to git. I still need to try private key files \ndirectly to see\nhow user interaction (like entering a passphrase or touching the U2F \nToken) would work.\n\n\n"},{"id":"429289","messageId":"YORsVNVC6lVEA7yD@camp.crustytoothpaste.net","threadId":"56054","inReplyTo":"pull.1041.git.git.1625559593910.gitgitgadget@gmail.com","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2021-07-06T14:44:36Z","receivedAt":"2021-07-06T14:45:06Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2021-07-06 at 08:19:53, Fabian Stelzer via GitGitGadget wrote:\n> From: Fabian Stelzer <fs@gigacodes.de>\n> \n> set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n> authorized_keys file) and commits/tags can be signed using the private\n> key from your ssh-agent.\n> \n> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n> verification.\n> \n> needs openssh>8.2p1\n\nUsually we'll want to write the explanation here in full sentences with\ntypical capitalization.\n\n> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n> ---\n>     RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen\n>     \n>     Support for using private keyfiles directly is still missing and i'm\n>     unsure on how to configure it or if the pubkey in the signingkey field\n>     is such a good idea. A SSH Fingerprint as signingkey would be nicer, but\n>     key lookup would be quite cumbersome. Maybe storing the fingerprint in\n>     signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile\n>     setting? As a default we could get the first ssh key from ssh-add and\n>     store it in the config to avoid unintentional changes of the used\n>     signing key. I've started with some tests for SSH Signing but having\n>     static private keyfiles would make this a lot easier. So still on my\n>     TODO.\n\nI think user.signingKey could be helpful for signing here.  That could\nbe a file name, not just a fingerprint, although we'd probably want to\nhave support for tilde expansion.  You could add an additional option,\ngpg.ssh.keyring, that specifies the signatures to verify.  That would be\nnamed the same thing as a potential option of gpg.openpgp.keyring,\nwhich would be convenient.  Also, gpg.ssh.revokedKeyring could maybe be\nthe name for revoked keys?\n\n>     This feature makes git signing much more accessible to the average user.\n>     Usually they have a SSH Key for pushing code already. Using it for\n>     signing commits allows us to verify not only the transport but the\n>     pushed code as well. The allowed_signers file could be kept in the\n>     repository if all receives are verified (allowing only useris with valid\n>     signatures to add/change them) or outside if generated/managed\n>     differently. Tools like gitolite could optionally generate and enforce\n>     them from the already existing user ssh keys for example.\n>     \n>     In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n>     signing/encryption and ssh keys which i think is quite common (at least\n>     for the email part). This way we can establish the correct trust for the\n>     SSH Keys without setting up a separate GPG Infrastructure (which is\n>     still quite painful for users) or implementing x509 signing support for\n>     git (which lacks good forwarding mechanisms). Using ssh agent forwarding\n>     makes this feature easily usable in todays development environments\n>     where code is often checked out in remote VMs / containers.\n\nI think some of this rationale would work well in the commit message,\nespecially the part about the fact that using an SSH key may be easier\nfor users and the fact that it can be well supported by smart cards.\nThose are compelling arguments about why this is a desirable change, and\nshould be in the commit message.\n\nI haven't looked too deeply at the intricacies of the change, but I'm in\nfavor of it.  I would, however, like to see some tests here, including\nfor commits, tags, and push certificates.  Note that you'll probably\nneed to run the testsuite both with and without\nGIT_TEST_DEFAULT_HASH=sha256 to verify everything works.\n\n> diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\n> index d94025cb368..fd71bd782ec 100644\n> --- a/Documentation/config/gpg.txt\n> +++ b/Documentation/config/gpg.txt\n> @@ -11,13 +11,13 @@ gpg.program::\n>  \n>  gpg.format::\n>  \tSpecifies which key format to use when signing with `--gpg-sign`.\n> -\tDefault is \"openpgp\" and another possible value is \"x509\".\n> +\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n>  \n>  gpg.<format>.program::\n>  \tUse this to customize the program used for the signing format you\n>  \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n>  \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n> -\tvalue for `gpg.x509.program` is \"gpgsm\".\n> +\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n>  \n>  gpg.minTrustLevel::\n>  \tSpecifies a minimum trust level for signature verification.  If\n> @@ -27,6 +27,15 @@ gpg.minTrustLevel::\n>  \twith at least `undefined` trust.  Setting this option overrides\n>  \tthe required trust-level for all operations.  Supported values,\n>  \tin increasing order of significance:\n> +\n> +gpg.ssh.allowedSigners::\n> +\tA file containing all valid SSH signing principals. \n> +\tSimilar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.\n> +\tDefaults to .gitsigners\n\nWe probably don't want to store this in the repository.  If OpenSSH has\na standard location for this, then we can default to that; otherwise, we\nshould pick something in .ssh or in $XDG_CONFIG_HOME/git.\n-- \nbrian m. carlson (he/him or they/them)\nToronto, Ontario, CA\n"},{"id":"429291","messageId":"xmqqzguzlc03.fsf@gitster.g","threadId":"56054","inReplyTo":"pull.1041.git.git.1625559593910.gitgitgadget@gmail.com","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-06T15:04:28Z","receivedAt":"2021-07-06T15:04:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n> authorized_keys file) and commits/tags can be signed using the private\n> key from your ssh-agent.\n>\n> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n> verification.\n\nThere are probably style and coding-guideline nit people will pick\nin the patch, but first of all I have to say that I am uncomfortably\nexcited to see this addition.\n\nOne thing that is unclear is how the 'allowed-signers' is expected\nto be maintained in the larger picture.  Who decides which keys\n(belong to whom) are trustworthy?  Does a contributor has to agree\nwith the decision that certain keys are trustworthy made by somebody\nelse in the project and use the same 'allowed-signers' collection of\nkeys to effectively participate in the project?  How do revoking and\nrotating keys work?\n\nIt was a deliberate design decision to let PGP infrastructure that\nis used to sign and verify signatures when we use PGP for signing\nwithout tying any of these decisions to the tracked contents, as\nthat would reduce the attack surface for a malicious tree contents\nto affect the signing and verification (in other words, \"we punted\"\n;-).  Even though I am not sure exactly what you meant by \"defaults\nto .gitsigners\", I am assuming that you meant a file with the name\nat the top-level of the working tree, which makes me worried, as it\nopens us to the risk of reading from and blindly trusting whatever\nsomebody else placed in the tree contents immediately after we \"git\npull\" (or \"git clone\").\n\nThanks for working on it.\n"},{"id":"429295","messageId":"a45b940f-490b-0b8a-a994-f78d1bb8f90a@gigacodes.de","threadId":"56054","inReplyTo":"YORsVNVC6lVEA7yD@camp.crustytoothpaste.net","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-06T15:33:51Z","receivedAt":"2021-07-06T15:33:56Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n>> set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n>> authorized_keys file) and commits/tags can be signed using the private\n>> key from your ssh-agent.\n>>\n>> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n>> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n>> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n>> verification.\n>>\n>> needs openssh>8.2p1\n> Usually we'll want to write the explanation here in full sentences with\n> typical capitalization.\nThanks, i was unsure about what to put in the commit and what into the \ncover letter.\nI'll fix this with the next patch update and move some of it into the \ncommit message.\nIn our env the commit messages are usually kept quite short.\n>\n>> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n>> ---\n>>      RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen\n>>      \n>>      Support for using private keyfiles directly is still missing and i'm\n>>      unsure on how to configure it or if the pubkey in the signingkey field\n>>      is such a good idea. A SSH Fingerprint as signingkey would be nicer, but\n>>      key lookup would be quite cumbersome. Maybe storing the fingerprint in\n>>      signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile\n>>      setting? As a default we could get the first ssh key from ssh-add and\n>>      store it in the config to avoid unintentional changes of the used\n>>      signing key. I've started with some tests for SSH Signing but having\n>>      static private keyfiles would make this a lot easier. So still on my\n>>      TODO.\n> I think user.signingKey could be helpful for signing here.  That could\n> be a file name, not just a fingerprint, although we'd probably want to\n> have support for tilde expansion.  You could add an additional option,\n> gpg.ssh.keyring, that specifies the signatures to verify.  That would be\n> named the same thing as a potential option of gpg.openpgp.keyring,\n> which would be convenient.  Also, gpg.ssh.revokedKeyring could maybe be\n> the name for revoked keys?\nThe problem ist that looking up a key by fingerprint alone is not really \npossible with ssh :/\nA referenced file (which could contain a public or private key) would be \nfine and i could return the fingerprint in the get_signing_key api which \nthe pushcerts code uses as \"pusher\" info in the cert.\nI'll change the keyring naming to what you suggested. Makes sense to \nhave this option for gpg as well.\n\n>\n>>      This feature makes git signing much more accessible to the average user.\n>>      Usually they have a SSH Key for pushing code already. Using it for\n>>      signing commits allows us to verify not only the transport but the\n>>      pushed code as well. The allowed_signers file could be kept in the\n>>      repository if all receives are verified (allowing only useris with valid\n>>      signatures to add/change them) or outside if generated/managed\n>>      differently. Tools like gitolite could optionally generate and enforce\n>>      them from the already existing user ssh keys for example.\n>>      \n>>      In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n>>      signing/encryption and ssh keys which i think is quite common (at least\n>>      for the email part). This way we can establish the correct trust for the\n>>      SSH Keys without setting up a separate GPG Infrastructure (which is\n>>      still quite painful for users) or implementing x509 signing support for\n>>      git (which lacks good forwarding mechanisms). Using ssh agent forwarding\n>>      makes this feature easily usable in todays development environments\n>>      where code is often checked out in remote VMs / containers.\n> I think some of this rationale would work well in the commit message,\n> especially the part about the fact that using an SSH key may be easier\n> for users and the fact that it can be well supported by smart cards.\n> Those are compelling arguments about why this is a desirable change, and\n> should be in the commit message.\n>\n> I haven't looked too deeply at the intricacies of the change, but I'm in\n> favor of it.  I would, however, like to see some tests here, including\n> for commits, tags, and push certificates.  Note that you'll probably\n> need to run the testsuite both with and without\n> GIT_TEST_DEFAULT_HASH=sha256 to verify everything works.\nI'm working on some tests but there are lots of GPG / GPGSM tests in the \nsuite and i'm unsure of how many i should duplicate.\nThanks for the info with the hash setting.\n>\n>> diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\n>> index d94025cb368..fd71bd782ec 100644\n>> --- a/Documentation/config/gpg.txt\n>> +++ b/Documentation/config/gpg.txt\n>> @@ -11,13 +11,13 @@ gpg.program::\n>>   \n>>   gpg.format::\n>>   \tSpecifies which key format to use when signing with `--gpg-sign`.\n>> -\tDefault is \"openpgp\" and another possible value is \"x509\".\n>> +\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n>>   \n>>   gpg.<format>.program::\n>>   \tUse this to customize the program used for the signing format you\n>>   \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n>>   \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n>> -\tvalue for `gpg.x509.program` is \"gpgsm\".\n>> +\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n>>   \n>>   gpg.minTrustLevel::\n>>   \tSpecifies a minimum trust level for signature verification.  If\n>> @@ -27,6 +27,15 @@ gpg.minTrustLevel::\n>>   \twith at least `undefined` trust.  Setting this option overrides\n>>   \tthe required trust-level for all operations.  Supported values,\n>>   \tin increasing order of significance:\n>> +\n>> +gpg.ssh.allowedSigners::\n>> +\tA file containing all valid SSH signing principals.\n>> +\tSimilar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.\n>> +\tDefaults to .gitsigners\n> We probably don't want to store this in the repository.  If OpenSSH has\n> a standard location for this, then we can default to that; otherwise, we\n> should pick something in .ssh or in $XDG_CONFIG_HOME/git.\nI'm not aware of a standard location. I think there are use cases to \nstore this in the repo, but i'm of course fine not defaulting to it.\n"},{"id":"429297","messageId":"fffd8c26-f3a7-b074-f4ba-e8552ca1d7cc@gigacodes.de","threadId":"56054","inReplyTo":"xmqqzguzlc03.fsf@gitster.g","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-06T15:45:41Z","receivedAt":"2021-07-06T15:45:46Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n>> authorized_keys file) and commits/tags can be signed using the private\n>> key from your ssh-agent.\n>>\n>> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n>> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n>> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n>> verification.\n> There are probably style and coding-guideline nit people will pick\n> in the patch, but first of all I have to say that I am uncomfortably\n> excited to see this addition.\n>\n> One thing that is unclear is how the 'allowed-signers' is expected\n> to be maintained in the larger picture.  Who decides which keys\n> (belong to whom) are trustworthy?  Does a contributor has to agree\n> with the decision that certain keys are trustworthy made by somebody\n> else in the project and use the same 'allowed-signers' collection of\n> keys to effectively participate in the project?  How do revoking and\n> rotating keys work?\n>\n> It was a deliberate design decision to let PGP infrastructure that\n> is used to sign and verify signatures when we use PGP for signing\n> without tying any of these decisions to the tracked contents, as\n> that would reduce the attack surface for a malicious tree contents\n> to affect the signing and verification (in other words, \"we punted\"\n> ;-).  Even though I am not sure exactly what you meant by \"defaults\n> to .gitsigners\", I am assuming that you meant a file with the name\n> at the top-level of the working tree, which makes me worried, as it\n> opens us to the risk of reading from and blindly trusting whatever\n> somebody else placed in the tree contents immediately after we \"git\n> pull\" (or \"git clone\").\n>\n> Thanks for working on it.\nGlad to hear that :)\nI tried to keep the style with the existing code but the IDE sometimes \nhas its own idea.\n\nI think there are two basic options for maintaining the allowed signers \nfile:\n1. Every developer has their own stored outside of the repo and \nadds/revokes trust manually like with gpg.\n     A central repo would probably verify against a list managed by the \ntool (e.g. gitolite)\n2. Store it in a .gitsigners file in the repo. This would only work if \nyou only allow signed commits/pushes from this point onwards. But this \nway a shared understading of trusted users can be maintained easily.\n     Only already trusted committers can add new users or change their \nown keys. The signers file is basically a ssh_authorized_keys file with \nan additional principal identifier added at the front like:\n     fs@gigacodes.de ssh-rsa XXXKEYXXX Comment\n     a@b.com ssh-ed25519 XXXKEYXXX Comment\n\nWhere are commits usually verified at the moment? On every devs checkout \nor only centrally on pushes?\n\nThe signers file also supports SSH CA keys and wildcard identifiers. At \nthe moment i look up the principal dynamically via the public key so \nit's just a text info of who's key is it at the moment.\nThe SSH CA Stuff is probably a niche use case but could be cool in a \ncorporate setting. Thats also what the revocation file is used for. The \nSSH CA can generate a KRL (like crl) which you put into it or you can \nspecify explicit public keys in it to deny them.\n"},{"id":"429301","messageId":"xmqq5yxnl43l.fsf@gitster.g","threadId":"56054","inReplyTo":"fffd8c26-f3a7-b074-f4ba-e8552ca1d7cc@gigacodes.de","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-06T17:55:10Z","receivedAt":"2021-07-06T17:55:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n>> One thing that is unclear is how the 'allowed-signers' is expected\n>> to be maintained in the larger picture.  Who decides which keys\n>> ...\n>> Thanks for working on it.\n> Glad to hear that :)\n\nThanks for explaining your thoughts (omitted).  When I say \"this is\nunclear\" in my response to a patch, I expect that unclear-ness will\nbe shared by other readers of the code, the doc and/or the log\nmessage, so please make sure an updated patch will reduce the need\nto ask the same question by future readers.\n\n> I tried to keep the style with the existing code but the IDE sometimes\n> has its own idea.\n\nDocumentation/CodingGuidelines and Documentation/SubmittingPatches\nwould hopefully help (if not, please ask and/or suggest clarification\non these documents).\n\nThanks.\n"},{"id":"429329","messageId":"04d901d7729e$a51a9160$ef4fb420$@nexbridge.com","threadId":"56054","inReplyTo":"fffd8c26-f3a7-b074-f4ba-e8552ca1d7cc@gigacodes.de","subject":"RE: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2021-07-06T19:39:27Z","receivedAt":"2021-07-06T19:39:43Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On July 6, 2021 11:46 AM, Fabian Stelzer wrote:\n>> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>\n>>> From: Fabian Stelzer <fs@gigacodes.de>\n>>>\n>>> set gpg.format = ssh and user.signingkey to a ssh public key string\n>>> (like from an authorized_keys file) and commits/tags can be signed\n>>> using the private key from your ssh-agent.\n>>>\n>>> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n>>> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners A\n>>> possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n>>> verification.\n>> There are probably style and coding-guideline nit people will pick in\n>> the patch, but first of all I have to say that I am uncomfortably\n>> excited to see this addition.\n>>\n>> One thing that is unclear is how the 'allowed-signers' is expected to\n>> be maintained in the larger picture.  Who decides which keys (belong\n>> to whom) are trustworthy?  Does a contributor has to agree with the\n>> decision that certain keys are trustworthy made by somebody else in\n>> the project and use the same 'allowed-signers' collection of keys to\n>> effectively participate in the project?  How do revoking and rotating\n>> keys work?\n>>\n>> It was a deliberate design decision to let PGP infrastructure that is\n>> used to sign and verify signatures when we use PGP for signing without\n>> tying any of these decisions to the tracked contents, as that would\n>> reduce the attack surface for a malicious tree contents to affect the\n>> signing and verification (in other words, \"we punted\"\n>> ;-).  Even though I am not sure exactly what you meant by \"defaults to\n>> .gitsigners\", I am assuming that you meant a file with the name at the\n>> top-level of the working tree, which makes me worried, as it opens us\n>> to the risk of reading from and blindly trusting whatever somebody\n>> else placed in the tree contents immediately after we \"git pull\" (or\n>> \"git clone\").\n>>\n>> Thanks for working on it.\n>Glad to hear that :)\n>I tried to keep the style with the existing code but the IDE sometimes has its own idea.\n>\n>I think there are two basic options for maintaining the allowed signers\n>file:\n>1. Every developer has their own stored outside of the repo and adds/revokes trust manually like with gpg.\n>     A central repo would probably verify against a list managed by the tool (e.g. gitolite) 2. Store it in a .gitsigners file in the repo. This\n>would only work if you only allow signed commits/pushes from this point onwards. But this way a shared understading of trusted users can\n>be maintained easily.\n>     Only already trusted committers can add new users or change their own keys. The signers file is basically a ssh_authorized_keys file\n>with an additional principal identifier added at the front like:\n>     fs@gigacodes.de ssh-rsa XXXKEYXXX Comment\n>     a@b.com ssh-ed25519 XXXKEYXXX Comment\n>\n>Where are commits usually verified at the moment? On every devs checkout or only centrally on pushes?\n>\n>The signers file also supports SSH CA keys and wildcard identifiers. At the moment i look up the principal dynamically via the public key so\n>it's just a text info of who's key is it at the moment.\n>The SSH CA Stuff is probably a niche use case but could be cool in a corporate setting. Thats also what the revocation file is used for. The\n>SSH CA can generate a KRL (like crl) which you put into it or you can specify explicit public keys in it to deny them.\n\nJust musing here... If adding SSH CA, would not adding support for a self-signed SSL CA make sense? In such a situation, a self-signed certificate can be created at an organizational level, or even from an official root CA. Per-user self-signed certificates, or organizationally defined CAs and certificates, could be created that are more stable than SSH CAs. Then something like OpenSSL (via libcurl) could handle the signature and validation management. Signed content could propagate to Cloud-based git servers and retain their ability to be property verified. Although I can see a drawback here, which relates to expiring certificates - although the concept of an expired signed content is somewhat compelling. Imaging the use case where a company has an employee who signs a tag/commit. The employee departs/retires/terminated/etc., and with it the published certificate is also revoked - an extreme case perhaps, but if the code can no longer be trusted by virtue of the termination, maybe t\n his is semantically interesting. This could be a core git function with no additional dependencies.\n\nRegards,\nRandall\n\n"},{"id":"429404","messageId":"92d8ae43-f146-e938-d793-b8b67d810130@gmail.com","threadId":"56054","inReplyTo":"pull.1041.git.git.1625559593910.gitgitgadget@gmail.com","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-07-07T06:26:52Z","receivedAt":"2021-07-07T06:27:11Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 06/07/21 15.19, Fabian Stelzer via GitGitGadget wrote:\n> From: Fabian Stelzer <fs@gigacodes.de>\n> \n> set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n> authorized_keys file) and commits/tags can be signed using the private\n> key from your ssh-agent.\n> \n> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n> verification.\n> \n> needs openssh>8.2p1\n> \n\nWhy did you choose to implement SSH-based signing as GPG interface? Why \nnot create similar one?\n\nIf at later times we need to implement other signing methods (besides \nGPG and SSH), we can refactor gpg-interface into generic signing \ninterface (say `signing.h`) and let each signing methods implement from it.\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"429406","messageId":"f4a5f0a5-8713-4640-e351-69b265c74460@gigacodes.de","threadId":"56054","inReplyTo":"92d8ae43-f146-e938-d793-b8b67d810130@gmail.com","subject":"Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-07T08:48:43Z","receivedAt":"2021-07-07T08:48:55Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 07.07.21 08:26, Bagas Sanjaya wrote:\n> On 06/07/21 15.19, Fabian Stelzer via GitGitGadget wrote:\n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> set gpg.format = ssh and user.signingkey to a ssh public key string \n>> (like from an\n>> authorized_keys file) and commits/tags can be signed using the private\n>> key from your ssh-agent.\n>>\n>> Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n>> defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n>> A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n>> verification.\n>>\n>> needs openssh>8.2p1\n>>\n>\n> Why did you choose to implement SSH-based signing as GPG interface? \n> Why not create similar one?\n>\n> If at later times we need to implement other signing methods (besides \n> GPG and SSH), we can refactor gpg-interface into generic signing \n> interface (say `signing.h`) and let each signing methods implement \n> from it. \nI agree that a general purpose \"signing\" would be cleaner. The GPG \nkewords are scattered all over the codebase but all the paths i found \njust call the generic sign_buffer / verify_signed_buffer from \ngpg-interface.c in the end whose api works quite well for other signing \nmechanisms as well. I will rename some struct fields to be more generic \nand adjust a few messages printed to the user which currently say things \nlike \"gpg failed to sign the data\" or \"has a gpg signature\" to be \ngeneric. Do we just want to call this \"signature\" and remove the gpg \nprefix or would that be too generic?\n\nRefactoring the whole gpg part to a generic \"signing\" would be quite \ninvolved and should probably be a different patch even though its mostly \nrenaming stuff.\nIf we want to go into that direction i could add the new config keys \nunder signing.* (signing.format = ssh|gpg, ...) and keep the \ncompatibility for the older gpg.* keys.\n"},{"id":"429742","messageId":"pull.1041.v2.git.git.1626092359713.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.git.git.1625559593910.gitgitgadget@gmail.com","subject":"[PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-12T12:19:19Z","receivedAt":"2021-07-12T12:19:45Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nOpenssh v8.2p1 added some new options to ssh-keygen for signature\ncreation and verification. These allow us to use ssh keys for git\nsignatures easily.\n\nSet gpg.format = ssh and user.signingkey to either a ssh public key\nstring (like from an authorized_keys file), or a ssh key file.\nIf the key file or the config value itself contains only a public key\nthen the private key needs to be available via ssh-agent.\nIf no signingkey is set then git will call 'ssh-add -L' to check for\navailable agent keys and use the first one for signing.\n\nVerification uses the gpg.ssh.keyring file (see ssh-keygen(1) \"ALLOWED\nSIGNERS\") which contains valid public keys and an principal (usually\nuser@domain). Depending on the environment this file can be managed by\nthe individual developer or for example generated by the central\nrepository server from known ssh keys with push access. If the\nrepository only allows signed commits / pushes then the file can even be\nstored inside it.\n\nTo revoke a key put the public key without the principal prefix into\ngpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n\"KEY REVOCATION LISTS\"). The same considerations about who to trust for\nverification as with the keyring file apply.\n\nThis feature makes git signing much more accessible to the average user.\nUsually they have a SSH Key for pushing code already. Using it\nfor signing commits allows us to verify not only the transport but the\npushed code as well.\n\nIn our corporate environemnt we use PIV x509 Certs on Yubikeys for email\nsigning/encryption and ssh keys which i think is quite common\n(at least for the email part). This way we can establish the correct\ntrust for the SSH Keys without setting up a separate GPG Infrastructure\n(which is still quite painful for users) or implementing x509 signing\nsupport for git (which lacks good forwarding mechanisms).\nUsing ssh agent forwarding makes this feature easily usable in todays\ndevelopment environments where code is often checked out in remote VMs / containers.\nIn such a setup the keyring & revocationKeyring can be centrally\ngenerated from the x509 CA information and distributed to the users.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n    RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen\n    \n    I have added support for using keyfiles directly, lots of tests and\n    generally cleaned up the signing & verification code a lot.\n    \n    I can still rename things from being gpg specific to a more general\n    \"signing\" but thats rather cosmetic. Also i'm not sure if i named the\n    new test files correctly.\n    \n    There is a patch in the pipeline for openssh by Damien Miller that will\n    add valid-after, valid-before options to the allowed keys keyring. This\n    allows us to pass the commit timestamp to the verification call and make\n    key rollover possible and still be able to verify older commits. Set\n    valid-after=NOW when adding your key to the keyring and set valid-before\n    to make it fail if used after a certain date. Software like\n    gitolite/github or corporate automation can do this automatically when\n    ssh push keys are addded / removed\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v2\nPull-Request: https://github.com/git/git/pull/1041\n\nRange-diff vs v1:\n\n 1:  f238392bfa8 ! 1:  b8b16f8e6ec Add commit & tag signing/verification via SSH keys using ssh-keygen\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    Add commit & tag signing/verification via SSH keys using ssh-keygen\n     +    Add commit, tag & push signing/verification via SSH keys using ssh-keygen\n      \n     -    set gpg.format = ssh and user.signingkey to a ssh public key string (like from an\n     -    authorized_keys file) and commits/tags can be signed using the private\n     -    key from your ssh-agent.\n     +    Openssh v8.2p1 added some new options to ssh-keygen for signature\n     +    creation and verification. These allow us to use ssh keys for git\n     +    signatures easily.\n      \n     -    Verification uses a allowed_signers_file (see ssh-keygen(1)) which\n     -    defaults to .gitsigners but can be set via gpg.ssh.allowedsigners\n     -    A possible gpg.ssh.revocationfile is also passed to ssh-keygen on\n     -    verification.\n     +    Set gpg.format = ssh and user.signingkey to either a ssh public key\n     +    string (like from an authorized_keys file), or a ssh key file.\n     +    If the key file or the config value itself contains only a public key\n     +    then the private key needs to be available via ssh-agent.\n     +    If no signingkey is set then git will call 'ssh-add -L' to check for\n     +    available agent keys and use the first one for signing.\n      \n     -    needs openssh>8.2p1\n     +    Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) \"ALLOWED\n     +    SIGNERS\") which contains valid public keys and an principal (usually\n     +    user@domain). Depending on the environment this file can be managed by\n     +    the individual developer or for example generated by the central\n     +    repository server from known ssh keys with push access. If the\n     +    repository only allows signed commits / pushes then the file can even be\n     +    stored inside it.\n     +\n     +    To revoke a key put the public key without the principal prefix into\n     +    gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n     +    \"KEY REVOCATION LISTS\"). The same considerations about who to trust for\n     +    verification as with the keyring file apply.\n     +\n     +    This feature makes git signing much more accessible to the average user.\n     +    Usually they have a SSH Key for pushing code already. Using it\n     +    for signing commits allows us to verify not only the transport but the\n     +    pushed code as well.\n     +\n     +    In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n     +    signing/encryption and ssh keys which i think is quite common\n     +    (at least for the email part). This way we can establish the correct\n     +    trust for the SSH Keys without setting up a separate GPG Infrastructure\n     +    (which is still quite painful for users) or implementing x509 signing\n     +    support for git (which lacks good forwarding mechanisms).\n     +    Using ssh agent forwarding makes this feature easily usable in todays\n     +    development environments where code is often checked out in remote VMs / containers.\n     +    In such a setup the keyring & revocationKeyring can be centrally\n     +    generated from the x509 CA information and distributed to the users.\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     @@ Documentation/config/gpg.txt: gpg.program::\n       gpg.minTrustLevel::\n       \tSpecifies a minimum trust level for signature verification.  If\n      @@ Documentation/config/gpg.txt: gpg.minTrustLevel::\n     - \twith at least `undefined` trust.  Setting this option overrides\n     - \tthe required trust-level for all operations.  Supported values,\n     - \tin increasing order of significance:\n     + * `marginal`\n     + * `fully`\n     + * `ultimate`\n     ++\n     ++gpg.ssh.keyring::\n     ++\tA file containing all valid SSH public signing keys. \n     ++\tSimilar to an .ssh/authorized_keys file.\n     ++\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n     ++\tIf a signing key is found in this file then the trust level will\n     ++\tbe set to \"fully\". Otherwise if the key is not present\n     ++\tbut the signature is still valid then the trust level will be \"undefined\".\n     ++\n     ++\tThis file can be set to a location outside of the repository\n     ++\tand every developer maintains their own trust store.\n     ++\tA central repository server could generate this file automatically\n     ++\tfrom ssh keys with push\taccess to verify the code against.\n     ++\tIn a corporate setting this file is probably generated at a global location\n     ++\tfrom some automation that already handles developer ssh keys. \n     ++\t\n     ++\tA repository that is only allowing signed commits can store the file \n     ++\tin the repository itself using a relative path. This way only committers\n     ++\twith an already valid key can add or change keys in the keyring.\n      +\n     -+gpg.ssh.allowedSigners::\n     -+\tA file containing all valid SSH signing principals. \n     -+\tSimilar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.\n     -+\tDefaults to .gitsigners\n     ++\tUsing a SSH CA key with the cert-authority option \n     ++\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n      +\n     -+gpg.ssh.revocationFile::\n     -+\tEither a SSH KRL or a list of revoked public keys.\n     ++\tTo revoke a key place the public key without the principal into the \n     ++\trevocationKeyring.\n     ++\n     ++gpg.ssh.revocationKeyring::\n     ++\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n      +\tSee ssh-keygen(1) for details.\n     - +\n     - * `undefined`\n     - * `never`\n     ++\tIf a public key is found in this file then it will always be treated\n     ++\tas having trust level \"never\" and signatures will show as invalid.\n      \n       ## Documentation/config/user.txt ##\n      @@ Documentation/config/user.txt: user.signingKey::\n       \tcommit, you can override the default selection with this variable.\n       \tThis option is passed unchanged to gpg's --local-user parameter,\n       \tso you may specify a key using any method that gpg supports.\n     -+\tIf gpg.format is set to \"ssh\" this needs to contain the valid\n     -+\tssh public key (e.g.: \"ssh-rsa XXXXXX identifier\") which corresponds\n     -+\tto the private key used for signing. The private key needs to be available\n     -+\tvia ssh-agent. Direct private key files are not supported yet.\n     ++\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n     ++\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and \n     ++\tcorresponds to the private key used for signing. The private key \n     ++\tneeds to be available via ssh-agent. Alternatively it can be set to\n     ++\ta file containing a private key directly. If not set git will call \n     ++\t\"ssh-add -L\" and try to use the first key available.\n     +\n     + ## builtin/receive-pack.c ##\n     +@@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const char *value, void *cb)\n     + {\n     + \tint status = parse_hide_refs_config(var, value, \"receive\");\n     + \n     ++\tgit_gpg_config(var, value, NULL);\n     ++\n     + \tif (status)\n     + \t\treturn status;\n     + \n     +@@ builtin/receive-pack.c: static void prepare_push_cert_sha1(struct child_process *proc)\n     + \t\tbogs = parse_signed_buffer(push_cert.buf, push_cert.len);\n     + \t\tcheck_signature(push_cert.buf, bogs, push_cert.buf + bogs,\n     + \t\t\t\tpush_cert.len - bogs, &sigcheck);\n     +-\n     ++\t\t\n     + \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n     + \t}\n     + \tif (!is_null_oid(&push_cert_oid)) {\n     +\n     + ## fmt-merge-msg.c ##\n     +@@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)\n     + \t\t\tlen = payload.len;\n     + \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n     + \t\t\t\t\t sig.len, &sigc) &&\n     +-\t\t\t\t!sigc.gpg_output)\n     ++\t\t\t\t!sigc.output)\n     + \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n     + \t\t\telse\n     +-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n     ++\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n     + \t\t}\n     + \t\tsignature_check_clear(&sigc);\n     + \n      \n       ## gpg-interface.c ##\n      @@\n     + #include \"config.h\"\n     + #include \"run-command.h\"\n     + #include \"strbuf.h\"\n     ++#include \"dir.h\"\n     + #include \"gpg-interface.h\"\n     + #include \"sigchain.h\"\n       #include \"tempfile.h\"\n       \n       static char *configured_signing_key;\n     @@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n       };\n       \n       static struct gpg_format *use_format = &gpg_format[0];\n     +@@ gpg-interface.c: static struct gpg_format *get_format_by_sig(const char *sig)\n     + void signature_check_clear(struct signature_check *sigc)\n     + {\n     + \tFREE_AND_NULL(sigc->payload);\n     ++\tFREE_AND_NULL(sigc->output);\n     + \tFREE_AND_NULL(sigc->gpg_output);\n     + \tFREE_AND_NULL(sigc->gpg_status);\n     + \tFREE_AND_NULL(sigc->signer);\n      @@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,\n       \treturn 1;\n       }\n     @@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,\n      +\tconst char *output = NULL;\n      +\tchar *next = NULL;\n      +\n     -+\t// ssh-keysign output should be:\n     -+\t// Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n     ++\t/* ssh-keysign output should be:\n     ++\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n     ++\t * or for valid but unknown keys:\n     ++\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n     ++\t */\n      +\n     -+\toutput = xmemdupz(sigc->gpg_status, strcspn(sigc->gpg_status, \" \\n\"));\n     -+\tif (skip_prefix(sigc->gpg_status, \"Good \\\"git\\\" signature for \", &output)) {\n     ++\toutput = xmemdupz(sigc->output, strcspn(sigc->output, \" \\n\"));\n     ++\tif (skip_prefix(sigc->output, \"Good \\\"git\\\" signature for \", &output)) {\n     ++\t\t// Valid signature for a trusted signer\n      +\t\tsigc->result = 'G';\n     ++\t\tsigc->trust_level = TRUST_FULLY;\n      +\n     -+\t\tnext = strchrnul(output, ' ');\n     ++\t\tnext = strchrnul(output, ' '); // 'principal'\n      +\t\treplace_cstring(&sigc->signer, output, next);\n      +\t\toutput = next + 1;\n      +\t\tnext = strchrnul(output, ' '); // 'with'\n     @@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,\n      +\t\toutput = next + 1;\n      +\t\tnext = strchrnul(output, ' '); // 'key'\n      +\t\toutput = next + 1;\n     -+\t\tnext = strchrnul(output, ' '); // key\n     ++\t\tnext = strchrnul(output, '\\n'); // key\n      +\t\treplace_cstring(&sigc->fingerprint, output, next);\n     ++\t\treplace_cstring(&sigc->key, output, next);\n     ++\t} else if (skip_prefix(sigc->output, \"Good \\\"git\\\" signature with \", &output)) {\n     ++\t\t// Valid signature, but key unknown\n     ++\t\tsigc->result = 'G';\n     ++\t\tsigc->trust_level = TRUST_UNDEFINED;\n     ++\n     ++\t\tnext = strchrnul(output, ' '); // KEY Type\n     ++\t\toutput = next + 1;\n     ++\t\tnext = strchrnul(output, ' '); // 'key'\n     ++\t\toutput = next + 1;\n     ++\t\tnext = strchrnul(output, '\\n'); // key\n     ++\t\treplace_cstring(&sigc->fingerprint, output, next);\n     ++\t\treplace_cstring(&sigc->key, output, next);\n      +\t} else {\n      +\t\tsigc->result = 'B';\n     ++\t\tsigc->trust_level = TRUST_NEVER;\n      +\t}\n     -+\n     -+\t// SSH-Keygen prints onto stdout instead of stderr like the output code expects - so we just copy it over\n     -+\tfree(sigc->gpg_output);\n     -+\tsigc->gpg_output = xmemdupz(sigc->gpg_status, strlen(sigc->gpg_status));\n      +}\n      +\n       static void parse_gpg_output(struct signature_check *sigc)\n       {\n       \tconst char *buf = sigc->gpg_status;\n     -@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,\n     - \t\t\t\tstruct strbuf *gpg_output,\n     - \t\t\t\tstruct strbuf *gpg_status)\n     +@@ gpg-interface.c: error:\n     + \tFREE_AND_NULL(sigc->key);\n     + }\n     + \n     +-static int verify_signed_buffer(const char *payload, size_t payload_size,\n     +-\t\t\t\tconst char *signature, size_t signature_size,\n     +-\t\t\t\tstruct strbuf *gpg_output,\n     +-\t\t\t\tstruct strbuf *gpg_status)\n     ++static int verify_ssh_signature(struct signature_check *sigc, struct gpg_format *fmt,\n     ++\tconst char *payload, size_t payload_size,\n     ++\tconst char *signature, size_t signature_size)\n       {\n      -\tstruct child_process gpg = CHILD_PROCESS_INIT;\n     -+\tstruct child_process gpg = CHILD_PROCESS_INIT,\n     -+\t\t\t     ssh_keygen = CHILD_PROCESS_INIT;\n     - \tstruct gpg_format *fmt;\n     +-\tstruct gpg_format *fmt;\n     ++\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n       \tstruct tempfile *temp;\n       \tint ret;\n      -\tstruct strbuf buf = STRBUF_INIT;\n      +\tconst char *line;\n      +\tsize_t trust_size;\n      +\tchar *principal;\n     -+\tstruct strbuf buf = STRBUF_INIT,\n     -+\t\t      principal_out = STRBUF_INIT,\n     -+\t\t      principal_err = STRBUF_INIT;\n     ++\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n     ++\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n       \n       \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n       \tif (!temp)\n      @@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,\n     - \tif (!fmt)\n     - \t\tBUG(\"bad signature '%s'\", signature);\n     + \t\treturn -1;\n     + \t}\n       \n     --\tstrvec_push(&gpg.args, fmt->program);\n     --\tstrvec_pushv(&gpg.args, fmt->verify_args);\n     --\tstrvec_pushl(&gpg.args,\n     --\t\t     \"--status-fd=1\",\n     --\t\t     \"--verify\", temp->filename.buf, \"-\",\n     --\t\t     NULL);\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\t// Find the principal from the  signers\n     -+\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n     -+\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"find-principals\",\n     -+\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     +-\tfmt = get_format_by_sig(signature);\n     +-\tif (!fmt)\n     +-\t\tBUG(\"bad signature '%s'\", signature);\n     ++\t// Find the principal from the  signers\n     ++\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n     ++\t\t\t\t\t\"-Y\", \"find-principals\",\n     ++\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     ++\t\t\t\t\t\"-s\", temp->filename.buf,\n     ++\t\t\t\t\tNULL);\n     ++\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     ++\tif (strstr(ssh_keygen_err.buf, \"unknown option\")) {\n     ++\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n     ++\t}\n     ++\tif (ret || !ssh_keygen_out.len) {\n     ++\t\t// We did not find a matching principal in the keyring - Check without validation\n     ++\t\tchild_process_init(&ssh_keygen);\n     ++\t\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n     ++\t\t\t\t\t\t\"-Y\", \"check-novalidate\",\n     ++\t\t\t\t\t\t\"-n\", \"git\",\n      +\t\t\t\t\t\t\"-s\", temp->filename.buf,\n      +\t\t\t\t\t\tNULL);\n     -+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &principal_out, 0, &principal_err, 0);\n     -+\t\tif (strstr(principal_err.buf, \"unknown option\")) {\n     -+\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n     -+\t\t}\n     -+\t\tif (ret || !principal_out.len)\n     -+\t\t\tgoto out;\n     -+\n     -+\t\t/* Iterate over all lines */\n     -+\t\tfor (line = principal_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n     ++\t\tret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     ++\t} else {\n     ++\t\t// Check every principal we found (one per line)\n     ++\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n      +\t\t\twhile (*line == '\\n')\n      +\t\t\t\tline++;\n      +\t\t\tif (!*line)\n      +\t\t\t\tbreak;\n     - \n     --\tif (!gpg_status)\n     --\t\tgpg_status = &buf;\n     ++\n      +\t\t\ttrust_size = strcspn(line, \" \\n\");\n      +\t\t\tprincipal = xmemdupz(line, trust_size);\n     - \n     --\tsigchain_push(SIGPIPE, SIG_IGN);\n     --\tret = pipe_command(&gpg, payload, payload_size,\n     --\t\t\t   gpg_status, 0, gpg_output, 0);\n     --\tsigchain_pop(SIGPIPE);\n     -+\t\t\tstrvec_push(&gpg.args,fmt->program);\n     ++\n     ++\t\t\tchild_process_init(&ssh_keygen);\n     ++\t\t\tstrbuf_release(&ssh_keygen_out);\n     ++\t\t\tstrbuf_release(&ssh_keygen_err);\n     ++\t\t\tstrvec_push(&ssh_keygen.args,fmt->program);\n      +\t\t\t// We found principals - Try with each until we find a match\n     -+\t\t\tstrvec_pushl(&gpg.args, \"-Y\", \"verify\",\n     -+\t\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     -+\t\t\t\t\t\t\"-I\", principal,\n     -+\t\t\t\t\t\t\"-s\", temp->filename.buf,\n     -+\t\t\t\t\t\t NULL);\n     - \n     --\tdelete_tempfile(&temp);\n     -+\t\t\tif (ssh_revocation_file) {\n     -+\t\t\t\tstrvec_pushl(&gpg.args, \"-r\", ssh_revocation_file, NULL);\n     -+\t\t\t}\n     ++\t\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"verify\",\n     ++\t\t\t\t\t\t\t//TODO: sprintf(\"-Overify-time=%s\", commit->date...),\n     ++\t\t\t\t\t\t\t\"-n\", \"git\",\n     ++\t\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     ++\t\t\t\t\t\t\t\"-I\", principal,\n     ++\t\t\t\t\t\t\t\"-s\", temp->filename.buf,\n     ++\t\t\t\t\t\t\tNULL);\n      +\n     -+\t\t\tif (!gpg_status)\n     -+\t\t\t\tgpg_status = &buf;\n     ++\t\t\tif (ssh_revocation_file && file_exists(ssh_revocation_file)) {\n     ++\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\", ssh_revocation_file, NULL);\n     ++\t\t\t}\n      +\n      +\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     -+\t\t\tret = pipe_command(&gpg, payload, payload_size,\n     -+\t\t\t\t\t   gpg_status, 0, gpg_output, 0);\n     ++\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n     ++\t\t\t\t\t&ssh_keygen_out, 0, &ssh_keygen_err, 0);\n      +\t\t\tsigchain_pop(SIGPIPE);\n     - \n     --\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n     -+\t\t\tret |= !strstr(gpg_status->buf, \"Good\");\n     ++\n     ++\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n      +\t\t\tif (ret == 0)\n      +\t\t\t\tbreak;\n      +\t\t}\n     -+\t} else {\n     -+\t\tstrvec_push(&gpg.args, fmt->program);\n     -+\t\tstrvec_pushv(&gpg.args, fmt->verify_args);\n     -+\t\tstrvec_pushl(&gpg.args,\n     -+\t\t\t\t\"--status-fd=1\",\n     -+\t\t\t\t\"--verify\", temp->filename.buf, \"-\",\n     -+\t\t\t\tNULL);\n     ++\t}\n      +\n     -+\t\tif (!gpg_status)\n     -+\t\t\tgpg_status = &buf;\n     ++\tsigc->payload = xmemdupz(payload, payload_size);\n     ++\tstrbuf_stripspace(&ssh_keygen_out, 0);\n     ++\tstrbuf_stripspace(&ssh_keygen_err, 0);\n     ++\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n     ++\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n      +\n     -+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     -+\t\tret = pipe_command(&gpg, payload, payload_size, gpg_status, 0,\n     -+\t\t\t\t   gpg_output, 0);\n     -+\t\tsigchain_pop(SIGPIPE);\n     -+\t\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n     ++\t//sigc->gpg_output = strbuf_detach(&ssh_keygen_err, NULL); // This flip around is broken...\n     ++\tsigc->gpg_status = strbuf_detach(&ssh_keygen_out, NULL);\n     ++\n     ++\tparse_ssh_output(sigc);\n     ++\n     ++\tdelete_tempfile(&temp);\n     ++\tstrbuf_release(&ssh_keygen_out);\n     ++\tstrbuf_release(&ssh_keygen_err);\n     ++\n     ++\treturn ret;\n     ++}\n     ++\n     ++static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt, \n     ++\tconst char *payload, size_t payload_size,\n     ++\tconst char *signature, size_t signature_size)\n     ++{\n     ++\tstruct child_process gpg = CHILD_PROCESS_INIT;\n     ++\tstruct tempfile *temp;\n     ++\tint ret;\n     ++\tstruct strbuf gpg_out = STRBUF_INIT;\n     ++\tstruct strbuf gpg_err = STRBUF_INIT;\n     ++\n     ++\ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n     ++\tif (!temp)\n     ++\t\treturn error_errno(_(\"could not create temporary file\"));\n     ++\tif (write_in_full(temp->fd, signature, signature_size) < 0 ||\n     ++\t    close_tempfile_gently(temp) < 0) {\n     ++\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n     ++\t\t\t    temp->filename.buf);\n     ++\t\tdelete_tempfile(&temp);\n     ++\t\treturn -1;\n      +\t}\n     + \n     + \tstrvec_push(&gpg.args, fmt->program);\n     + \tstrvec_pushv(&gpg.args, fmt->verify_args);\n     + \tstrvec_pushl(&gpg.args,\n     +-\t\t     \"--status-fd=1\",\n     +-\t\t     \"--verify\", temp->filename.buf, \"-\",\n     +-\t\t     NULL);\n     +-\n     +-\tif (!gpg_status)\n     +-\t\tgpg_status = &buf;\n     ++\t\t\t\"--status-fd=1\",\n     ++\t\t\t\"--verify\", temp->filename.buf, \"-\",\n     ++\t\t\tNULL);\n     + \n     + \tsigchain_push(SIGPIPE, SIG_IGN);\n     +-\tret = pipe_command(&gpg, payload, payload_size,\n     +-\t\t\t   gpg_status, 0, gpg_output, 0);\n     ++\tret = pipe_command(&gpg, payload, payload_size, &gpg_out, 0,\n     ++\t\t\t\t&gpg_err, 0);\n     + \tsigchain_pop(SIGPIPE);\n     ++\tret |= !strstr(gpg_out.buf, \"\\n[GNUPG:] GOODSIG \");\n     + \n     +-\tdelete_tempfile(&temp);\n     ++\tsigc->payload = xmemdupz(payload, payload_size);\n     ++\tsigc->output = strbuf_detach(&gpg_err, NULL);\n     ++\tsigc->gpg_status = strbuf_detach(&gpg_out, NULL);\n     + \n     +-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n     +-\tstrbuf_release(&buf); /* no matter it was used or not */\n     ++\tparse_gpg_output(sigc);\n      +\n     -+out:\n      +\tdelete_tempfile(&temp);\n     -+\tstrbuf_release(&principal_out);\n     -+\tstrbuf_release(&principal_err);\n     - \tstrbuf_release(&buf); /* no matter it was used or not */\n     ++\tstrbuf_release(&gpg_out);\n     ++\tstrbuf_release(&gpg_err);\n       \n       \treturn ret;\n     -@@ gpg-interface.c: int check_signature(const char *payload, size_t plen, const char *signature,\n     - \tsigc->payload = xmemdupz(payload, plen);\n     - \tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n     - \tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n     + }\n     +@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,\n     + int check_signature(const char *payload, size_t plen, const char *signature,\n     + \tsize_t slen, struct signature_check *sigc)\n     + {\n     +-\tstruct strbuf gpg_output = STRBUF_INIT;\n     +-\tstruct strbuf gpg_status = STRBUF_INIT;\n     ++\tstruct gpg_format *fmt;\n     + \tint status;\n     + \n     + \tsigc->result = 'N';\n     + \tsigc->trust_level = -1;\n     + \n     +-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n     +-\t\t\t\t      &gpg_output, &gpg_status);\n     +-\tif (status && !gpg_output.len)\n     +-\t\tgoto out;\n     +-\tsigc->payload = xmemdupz(payload, plen);\n     +-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n     +-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n      -\tparse_gpg_output(sigc);\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\tparse_ssh_output(sigc);\n     ++\tfmt = get_format_by_sig(signature);\n     ++\tif (!fmt)\n     ++\t\tBUG(\"bad signature '%s'\", signature);\n     ++\n     ++\tif (!strcmp(fmt->name, \"ssh\")) {\n     ++\t\tstatus = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);\n      +\t} else {\n     -+\t\tparse_gpg_output(sigc);\n     ++\t\tstatus = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);\n      +\t}\n     ++\tif (status && !sigc->gpg_output)\n     ++\t\treturn !!status;\n     ++\n       \tstatus |= sigc->result != 'G';\n       \tstatus |= sigc->trust_level < configured_min_trust_level;\n       \n     +- out:\n     +-\tstrbuf_release(&gpg_status);\n     +-\tstrbuf_release(&gpg_output);\n     +-\n     + \treturn !!status;\n     + }\n     + \n     + void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n     + {\n     + \tconst char *output = flags & GPG_VERIFY_RAW ?\n     +-\t\tsigc->gpg_status : sigc->gpg_output;\n     ++\t\tsigc->gpg_status : sigc->output;\n     + \n     + \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n     + \t\tfputs(sigc->payload, stdout);\n      @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     + \tint ret;\n     + \n     + \tif (!strcmp(var, \"user.signingkey\")) {\n     ++\t\t/* user.signingkey can contain one of the following\n     ++\t\t * when format = openpgp/x509\n     ++\t\t *   - GPG KeyID\n     ++\t\t * when format = ssh\n     ++\t\t *   - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)\n     ++\t\t *   - path to a file containing a public or a private ssh key\n     ++\t\t */\n     + \t\tif (!value)\n     + \t\t\treturn config_error_nonbool(var);\n     + \t\tset_signing_key(value);\n       \t\treturn 0;\n       \t}\n       \n     -+\tif (!strcmp(var, \"gpg.ssh.allowedsigners\")) {\n     ++\tif (!strcmp(var, \"gpg.ssh.keyring\")) {\n     ++\t\tif (!value)\n     ++\t\t\treturn config_error_nonbool(var);\n      +\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n      +\t}\n      +\n     -+\tif (!strcmp(var, \"gpg.ssh.revocationfile\")) {\n     ++\tif (!strcmp(var, \"gpg.ssh.revocationkeyring\")) {\n     ++\t\tif (!value)\n     ++\t\t\treturn config_error_nonbool(var);\n      +\t\treturn git_config_string(&ssh_revocation_file, var, value);\n      +\t}\n      +\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n       \tif (fmtname) {\n       \t\tfmt = get_format_by_name(fmtname);\n       \t\treturn git_config_string(&fmt->program, var, value);\n     -@@ gpg-interface.c: const char *get_signing_key(void)\n     +@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     + \treturn 0;\n     + }\n     + \n     ++static char *get_ssh_key_fingerprint(const char *signing_key) {\n     ++\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n     ++\tint ret = -1;\n     ++\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n     ++\tstruct strbuf **fingerprint;\n     ++\n     ++\t/* For SSH Signing this can contain a filename or a public key\n     ++\t* For textual representation we usually want a fingerprint\n     ++\t*/\n     ++\tif (istarts_with(signing_key, \"ssh-\")) {\n     ++\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n     ++\t\t\t\t\t\"-lf\", \"-\",\n     ++\t\t\t\t\tNULL);\n     ++\t\tret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0,  NULL, 0);\n     ++\t} else {\n     ++\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n     ++\t\t\t\t\t\"-lf\", configured_signing_key,\n     ++\t\t\t\t\tNULL);\n     ++\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);\n     ++\t\tif (!!ret)\n     ++\t\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n     ++\t\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n     ++\t\tif (fingerprint[1]) {\n     ++\t\t\treturn strbuf_detach(fingerprint[1], NULL);\n     ++\t\t}\n     ++\t}\n     ++\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n     ++}\n     ++\n     ++// Returns the first public key from an ssh-agent to use for signing\n     ++static char *get_default_ssh_signing_key(void) {\n     ++\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n     ++\tint ret = -1;\n     ++\tstruct strbuf key_stdout = STRBUF_INIT;\n     ++\tstruct strbuf **keys;\n     ++\n     ++\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n     ++\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n     ++\tif (!ret) { \n     ++\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n     ++\t\tif (keys[0])\n     ++\t\t\treturn strbuf_detach(keys[0], NULL);\n     ++\t}\n     ++\n     ++\treturn \"\";\n     ++}\n     ++\n     ++// Returns a textual but unique representation ot the signing key\n     ++const char *get_signing_key_id(void) {\n     ++\tif (!strcmp(use_format->name, \"ssh\")) {\n     ++\t\treturn get_ssh_key_fingerprint(get_signing_key());\n     ++\t} else {\n     ++\t\t// GPG/GPGSM only store a key id on this variable\n     ++\t\treturn get_signing_key();\n     ++\t}\n     ++}\n     ++\n     + const char *get_signing_key(void)\n       {\n       \tif (configured_signing_key)\n       \t\treturn configured_signing_key;\n      -\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n      +\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\t// We could simply use the first key listed by ssh-add -L and risk signing with the wrong key\n     -+\t\treturn \"\";\n     ++\t\treturn get_default_ssh_signing_key();\n      +\t} else {\n      +\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n      +\t}\n     @@ gpg-interface.c: const char *get_signing_key(void)\n      +{\n      +\tif (ssh_allowed_signers)\n      +\t\treturn ssh_allowed_signers;\n     -+\treturn GPG_SSH_ALLOWED_SIGNERS;\n     ++\n     ++\tdie(\"A Path to an allowed signers ssh keyring is needed for validation\");\n       }\n       \n       int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n     @@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature\n       \tint ret;\n       \tsize_t i, j, bottom;\n       \tstruct strbuf gpg_status = STRBUF_INIT;\n     --\n     ++\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n     ++\tchar *ssh_signing_key_file = NULL;\n     ++\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n     ++\n     ++\tif (!strcmp(use_format->name, \"ssh\")) {\n     ++\t\tif (!signing_key || signing_key[0] == '\\0')\n     ++\t\t\treturn error(_(\"user.signingkey needs to be set for ssh signing\"));\n     ++\n     ++\n     ++\t\tif (istarts_with(signing_key, \"ssh-\")) {\n     ++\t\t\t// A literal ssh key\n     ++\t\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n     ++\t\t\tif (!temp)\n     ++\t\t\t\treturn error_errno(_(\"could not create temporary file\"));\n     ++\t\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) < 0 ||\n     ++\t\t\t\tclose_tempfile_gently(temp) < 0) {\n     ++\t\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"), temp->filename.buf);\n     ++\t\t\t\tdelete_tempfile(&temp);\n     ++\t\t\t\treturn -1;\n     ++\t\t\t}\n     ++\t\t\tssh_signing_key_file= temp->filename.buf;\n     ++\t\t} else {\n     ++\t\t\t// We assume a file\n     ++\t\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n     ++\t\t}\n     + \n      -\tstrvec_pushl(&gpg.args,\n      -\t\t     use_format->program,\n      -\t\t     \"--status-fd=2\",\n      -\t\t     \"-bsau\", signing_key,\n      -\t\t     NULL);\n     -+\tstruct tempfile *temp = NULL;\n     -+\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\tif (!signing_key)\n     -+\t\t\treturn error(_(\"user.signingkey needs to be set to a ssh public key for ssh signing\"));\n     -+\n     -+\t\t// signing_key is a public ssh key\n     -+\t\t// FIXME: Allow specifying a key file so we can use private keyfiles instead of ssh-agent\n     -+\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n     -+\t\tif (!temp)\n     ++\t\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n     ++\t\tif (!buffer_file)\n      +\t\t\treturn error_errno(_(\"could not create temporary file\"));\n     -+\t\tif (write_in_full(temp->fd, signing_key,\n     -+\t\t\t\t\tstrlen(signing_key)) < 0 ||\n     -+\t\t\tclose_tempfile_gently(temp) < 0) {\n     -+\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"), temp->filename.buf);\n     -+\t\t\tdelete_tempfile(&temp);\n     ++\t\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n     ++\t\t\tclose_tempfile_gently(buffer_file) < 0) {\n     ++\t\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"), buffer_file->filename.buf);\n     ++\t\t\tdelete_tempfile(&buffer_file);\n      +\t\t\treturn -1;\n      +\t\t}\n     ++\n      +\t\tstrvec_pushl(&gpg.args, use_format->program ,\n      +\t\t\t\t\t\"-Y\", \"sign\",\n      +\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\"-f\", temp->filename.buf,\n     ++\t\t\t\t\t\"-f\", ssh_signing_key_file,\n     ++\t\t\t\t\tbuffer_file->filename.buf,\n      +\t\t\t\t\tNULL);\n     ++\n     ++\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     ++\t\tret = pipe_command(&gpg, NULL, 0, NULL, 0, &gpg_status, 0);\n     ++\t\tsigchain_pop(SIGPIPE);\n     ++\n     ++\t\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n     ++\t\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n     ++\t\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n     ++\t\t\terror_errno(_(\"failed reading ssh signing data buffer from '%s'\"), ssh_signature_filename.buf);\n     ++\t\t}\n     ++\t\tunlink_or_warn(ssh_signature_filename.buf);\n     ++\t\tstrbuf_release(&ssh_signature_filename);\n     ++\t\tdelete_tempfile(&buffer_file);\n      +\t} else {\n      +\t\tstrvec_pushl(&gpg.args, use_format->program ,\n      +\t\t\t\t\t\"--status-fd=2\",\n      +\t\t\t\t\t\"-bsau\", signing_key,\n      +\t\t\t\t\tNULL);\n     ++\n     ++\t\t/*\n     ++\t\t* When the username signingkey is bad, program could be terminated\n     ++\t\t* because gpg exits without reading and then write gets SIGPIPE.\n     ++\t\t*/\n     ++\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     ++\t\tret = pipe_command(&gpg, buffer->buf, buffer->len, signature, 1024, &gpg_status, 0);\n     ++\t\tsigchain_pop(SIGPIPE);\n      +\t}\n       \n       \tbottom = signature->len;\n       \n     -@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n     - \t\t\t   signature, 1024, &gpg_status, 0);\n     - \tsigchain_pop(SIGPIPE);\n     - \n     --\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n     +-\t/*\n     +-\t * When the username signingkey is bad, program could be terminated\n     +-\t * because gpg exits without reading and then write gets SIGPIPE.\n     +-\t */\n     +-\tsigchain_push(SIGPIPE, SIG_IGN);\n     +-\tret = pipe_command(&gpg, buffer->buf, buffer->len,\n     +-\t\t\t   signature, 1024, &gpg_status, 0);\n     +-\tsigchain_pop(SIGPIPE);\n      +\tif (temp)\n      +\t\tdelete_tempfile(&temp);\n     -+\n     + \n     +-\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n      +\tif (!strcmp(use_format->name, \"ssh\")) {\n      +\t\tif (strstr(gpg_status.buf, \"unknown option\")) {\n      +\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n     @@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature\n       \t\treturn error(_(\"gpg failed to sign the data\"));\n      \n       ## gpg-interface.h ##\n     -@@ gpg-interface.h: struct strbuf;\n     - #define GPG_VERIFY_RAW\t\t\t2\n     - #define GPG_VERIFY_OMIT_STATUS\t4\n     +@@ gpg-interface.h: enum signature_trust_level {\n       \n     -+#define GPG_SSH_ALLOWED_SIGNERS \".gitsigners\"\n     -+\n     - enum signature_trust_level {\n     - \tTRUST_UNDEFINED,\n     - \tTRUST_NEVER,\n     + struct signature_check {\n     + \tchar *payload;\n     +-\tchar *gpg_output;\n     +-\tchar *gpg_status;\n     ++\tchar *output;\n     ++\tchar *gpg_output; // This will be printed in commit logs\n     ++\tchar *gpg_status; // Only used internally -> remove\n     + \n     + \t/*\n     + \t * possible \"result\":\n      @@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n       int git_gpg_config(const char *, const char *, void *);\n       void set_signing_key(const char *);\n       const char *get_signing_key(void);\n     ++\n     ++/* Returns a textual unique representation of the signing key in use\n     ++ * Either a GPG KeyID or a SSH Key Fingerprint\n     ++ */\n     ++const char *get_signing_key_id(void);\n     ++\n      +const char *get_ssh_allowed_signers(void);\n       int check_signature(const char *payload, size_t plen,\n       \t\t    const char *signature, size_t slen,\n       \t\t    struct signature_check *sigc);\n     +\n     + ## log-tree.c ##\n     +@@ log-tree.c: static void show_signature(struct rev_info *opt, struct commit *commit)\n     + \n     + \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n     + \t\t\t\t signature.len, &sigc);\n     +-\tif (status && !sigc.gpg_output)\n     ++\tif (status && !sigc.output)\n     + \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n     + \telse\n     +-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n     ++\t\tshow_sig_lines(opt, status, sigc.output);\n     + \tsignature_check_clear(&sigc);\n     + \n     +  out:\n     +@@ log-tree.c: static int show_one_mergetag(struct commit *commit,\n     + \t\t/* could have a good signature */\n     + \t\tstatus = check_signature(payload.buf, payload.len,\n     + \t\t\t\t\t signature.buf, signature.len, &sigc);\n     +-\t\tif (sigc.gpg_output)\n     +-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n     ++\t\tif (sigc.output)\n     ++\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n     + \t\telse\n     + \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n     + \t\tsignature_check_clear(&sigc);\n     +\n     + ## pretty.c ##\n     +@@ pretty.c: static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n     + \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n     + \t\tswitch (placeholder[1]) {\n     + \t\tcase 'G':\n     +-\t\t\tif (c->signature_check.gpg_output)\n     +-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n     ++\t\t\tif (c->signature_check.output)\n     ++\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n     + \t\t\tbreak;\n     + \t\tcase '?':\n     + \t\t\tswitch (c->signature_check.result) {\n     +\n     + ## send-pack.c ##\n     +@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,\n     + \tconst struct ref *ref;\n     + \tstruct string_list_item *item;\n     + \tchar *signing_key = xstrdup(get_signing_key());\n     ++\tchar *signing_key_id = xstrdup(get_signing_key_id());\n     + \tconst char *cp, *np;\n     + \tstruct strbuf cert = STRBUF_INIT;\n     + \tint update_seen = 0;\n     +-\n     ++\t\n     + \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n     +-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n     ++\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n     + \tdatestamp(&cert);\n     + \tstrbuf_addch(&cert, '\\n');\n     + \tif (args->url && *args->url) {\n     +@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,\n     + \n     + free_return:\n     + \tfree(signing_key);\n     ++\tfree(signing_key_id);\n     + \tstrbuf_release(&cert);\n     + \treturn update_seen;\n     + }\n     +\n     + ## t/lib-gpg.sh ##\n     +@@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '\n     + \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n     + '\n     + \n     ++test_lazy_prereq GPGSSH '\n     ++\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n     ++\ttest $? != 127 || exit 1\n     ++\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n     ++\ttest $? = 0 || exit 1; \n     ++\tmkdir -p \"${GNUPGHOME}\" &&\n     ++\tchmod 0700 \"${GNUPGHOME}\" &&\n     ++\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n     ++\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n     ++\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n     ++\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"principal_\\\" NR \\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n     ++\tcat \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n     ++\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n     ++'\n     ++\n     ++SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n     ++SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n     ++SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n     ++SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n     ++SIGNING_KEY_PASSPHRASE=\"super_secret\"\n     ++SIGNING_KEYRING=\"${GNUPGHOME}/ssh.all_valid.keyring\"\n     ++\n     ++GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n     ++GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n     ++KEY_NOT_TRUSTED=\"No principal matched\"\n     ++BAD_SIGNATURE=\"Signature verification failed\"\n     ++\n     + sanitize_pgp() {\n     + \tperl -ne '\n     + \t\t/^-----END PGP/ and $in_pgp = 0;\n     +\n     + ## t/t4202-log.sh ##\n     +@@ t/t4202-log.sh: test_expect_success GPGSM 'setup signed branch x509' '\n     + \tgit commit -S -m signed_commit\n     + '\n     + \n     ++test_expect_success GPGSSH 'setup sshkey signed branch' '\n     ++\ttest_config gpg.format ssh &&\n     ++\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\ttest_when_finished \"git reset --hard && git checkout main\" &&\n     ++\tgit checkout -b signed-ssh main &&\n     ++\techo foo >foo &&\n     ++\tgit add foo &&\n     ++\tgit commit -S -m signed_commit\n     ++'\n     ++\n     + test_expect_success GPGSM 'log x509 fingerprint' '\n     + \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n     + \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n     +@@ t/t4202-log.sh: test_expect_success GPGSM 'log --graph --show-signature x509' '\n     + \tgrep \"^| gpgsm: Good signature\" actual\n     + '\n     + \n     ++test_expect_success GPGSSH 'log ssh key fingerprint' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n     ++\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     + test_expect_success GPG 'log --graph --show-signature for merged tag' '\n     + \ttest_when_finished \"git reset --hard && git checkout main\" &&\n     + \tgit checkout -b plain main &&\n     +\n     + ## t/t5534-push-signed.sh ##\n     +@@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certificate' '\n     + \ttest_cmp expect dst/push-cert-status\n     + '\n     + \n     ++test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n     ++\tprepare_dst &&\n     ++\tmkdir -p dst/.git/hooks &&\n     ++\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit -C dst config receive.certnonceseed sekrit &&\n     ++\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n     ++\t# discard the update list\n     ++\tcat >/dev/null\n     ++\t# record the push certificate\n     ++\tif test -n \"${GIT_PUSH_CERT-}\"\n     ++\tthen\n     ++\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n     ++\tfi &&\n     ++\n     ++\tcat >../push-cert-status <<E_O_F\n     ++\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n     ++\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n     ++\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n     ++\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n     ++\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n     ++\tE_O_F\n     ++\n     ++\tEOF\n     ++\n     ++\ttest_config gpg.format ssh &&\n     ++\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\tgit push --signed dst noop ff +noff &&\n     ++\n     ++\t(\n     ++\t\tcat <<-\\EOF &&\n     ++\t\tSIGNER=principal_1\n     ++\t\tKEY=FINGERPRINT\n     ++\t\tSTATUS=G\n     ++\t\tNONCE_STATUS=OK\n     ++\t\tEOF\n     ++\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n     ++\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n     ++\n     ++\tnoop=$(git rev-parse noop) &&\n     ++\tff=$(git rev-parse ff) &&\n     ++\tnoff=$(git rev-parse noff) &&\n     ++\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n     ++\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n     ++\ttest_cmp expect dst/push-cert-status\n     ++'\n     ++\n     + test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n     + \t# First, invoke receive-pack with dummy input to obtain its preamble.\n     + \tprepare_dst &&\n     +@@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n     + \ttest_cmp expect dst/push-cert-status\n     + '\n     + \n     ++test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n     ++\ttest_config gpg.format ssh &&\n     ++\tprepare_dst &&\n     ++\tmkdir -p dst/.git/hooks &&\n     ++\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit -C dst config receive.certnonceseed sekrit &&\n     ++\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n     ++\t# discard the update list\n     ++\tcat >/dev/null\n     ++\t# record the push certificate\n     ++\tif test -n \"${GIT_PUSH_CERT-}\"\n     ++\tthen\n     ++\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n     ++\tfi &&\n     ++\n     ++\tcat >../push-cert-status <<E_O_F\n     ++\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n     ++\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n     ++\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n     ++\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n     ++\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n     ++\tE_O_F\n     ++\n     ++\tEOF\n     ++\n     ++\ttest_config user.email hasnokey@nowhere.com &&\n     ++\ttest_config gpg.format ssh &&\n     ++\t\n     ++\ttest_config user.signingkey \"\" &&\n     ++\t(\n     ++\t\tsane_unset GIT_COMMITTER_EMAIL &&\n     ++\t\ttest_must_fail git push --signed dst noop ff +noff\n     ++\t) &&\n     ++\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\tgit push --signed dst noop ff +noff &&\n     ++\n     ++\t(\n     ++\t\tcat <<-\\EOF &&\n     ++\t\tSIGNER=principal_1\n     ++\t\tKEY=FINGERPRINT\n     ++\t\tSTATUS=G\n     ++\t\tNONCE_STATUS=OK\n     ++\t\tEOF\n     ++\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n     ++\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n     ++\n     ++\tnoop=$(git rev-parse noop) &&\n     ++\tff=$(git rev-parse ff) &&\n     ++\tnoff=$(git rev-parse noff) &&\n     ++\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n     ++\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n     ++\ttest_cmp expect dst/push-cert-status\n     ++'\n     ++\n     + test_expect_success GPG 'failed atomic push does not execute GPG' '\n     + \tprepare_dst &&\n     + \tgit -C dst config receive.certnonceseed sekrit &&\n     +\n     + ## t/t7031-verify-tag-signed-ssh.sh (new) ##\n     +@@\n     ++#!/bin/sh\n     ++\n     ++test_description='signed tag tests'\n     ++GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n     ++export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     ++\n     ++. ./test-lib.sh\n     ++. \"$TEST_DIRECTORY/lib-gpg.sh\"\n     ++\n     ++test_expect_success GPGSSH 'create signed tags ssh' '\n     ++\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n     ++\ttest_config gpg.format ssh &&\n     ++\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\n     ++\techo 1 >file && git add file &&\n     ++\ttest_tick && git commit -m initial &&\n     ++\tgit tag -s -m initial initial &&\n     ++\tgit branch side &&\n     ++\n     ++\techo 2 >file && test_tick && git commit -a -m second &&\n     ++\tgit tag -s -m second second &&\n     ++\n     ++\tgit checkout side &&\n     ++\techo 3 >elif && git add elif &&\n     ++\ttest_tick && git commit -m \"third on side\" &&\n     ++\n     ++\tgit checkout main &&\n     ++\ttest_tick && git merge -S side &&\n     ++\tgit tag -s -m merge merge &&\n     ++\n     ++\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n     ++\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n     ++\n     ++\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n     ++\tgit tag -s -m fourth fourth-signed &&\n     ++\n     ++\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n     ++\tgit tag fifth-unsigned &&\n     ++\n     ++\tgit config commit.gpgsign true &&\n     ++\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n     ++\tgit tag -a -m sixth sixth-unsigned &&\n     ++\n     ++\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n     ++\tgit tag -m seventh -s seventh-signed &&\n     ++\n     ++\techo 8 >file && test_tick && git commit -a -m eighth &&\n     ++\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'verify and show ssh signatures' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.mintrustlevel UNDEFINED &&\n     ++\t(\n     ++\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n     ++\t\tdo\n     ++\t\t\tgit verify-tag $tag 2>actual &&\n     ++\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\techo $tag OK || exit 1\n     ++\t\tdone\n     ++\t) &&\n     ++\t(\n     ++\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n     ++\t\tdo\n     ++\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n     ++\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\techo $tag OK || exit 1\n     ++\t\tdone\n     ++\t) &&\n     ++\t(\n     ++\t\tfor tag in eighth-signed-alt\n     ++\t\tdo\n     ++\t\t\tgit verify-tag $tag 2>actual &&\n     ++\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n     ++\t\t\techo $tag OK || exit 1\n     ++\t\tdone\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'detect fudged ssh signature' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit cat-file tag seventh-signed >raw &&\n     ++\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n     ++\tgit hash-object -w -t tag forged1 >forged1.tag &&\n     ++\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n     ++\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n     ++\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n     ++\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n     ++'\n     ++\n     ++# test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n     ++# \ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++# \t(\n     ++# \t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n     ++# \t\tdo\n     ++# \t\t\tgit verify-tag --raw $tag 2>actual &&\n     ++# \t\t\tgrep \"GOODSIG\" actual &&\n     ++# \t\t\t! grep \"BADSIG\" actual &&\n     ++# \t\t\techo $tag OK || exit 1\n     ++# \t\tdone\n     ++# \t) &&\n     ++# \t(\n     ++# \t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n     ++# \t\tdo\n     ++# \t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n     ++# \t\t\t! grep \"GOODSIG\" actual &&\n     ++# \t\t\t! grep \"BADSIG\" actual &&\n     ++# \t\t\techo $tag OK || exit 1\n     ++# \t\tdone\n     ++# \t) &&\n     ++# \t(\n     ++# \t\tfor tag in eighth-signed-alt\n     ++# \t\tdo\n     ++# \t\t\tgit verify-tag --raw $tag 2>actual &&\n     ++# \t\t\tgrep \"GOODSIG\" actual &&\n     ++# \t\t\t! grep \"BADSIG\" actual &&\n     ++# \t\t\tgrep \"TRUST_UNDEFINED\" actual &&\n     ++# \t\t\techo $tag OK || exit 1\n     ++# \t\tdone\n     ++# \t)\n     ++# '\n     ++\n     ++# test_expect_success GPGSM 'verify signatures with --raw x509' '\n     ++# \tgit verify-tag --raw ninth-signed-x509 2>actual &&\n     ++# \tgrep \"GOODSIG\" actual &&\n     ++# \t! grep \"BADSIG\" actual &&\n     ++# \techo ninth-signed-x509 OK\n     ++# '\n     ++\n     ++# test_expect_success GPGSSH 'verify multiple tags' '\n     ++# \ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++# \ttags=\"fourth-signed sixth-signed seventh-signed\" &&\n     ++# \tfor i in $tags\n     ++# \tdo\n     ++# \t\tgit verify-tag -v --raw $i || return 1\n     ++# \tdone >expect.stdout 2>expect.stderr.1 &&\n     ++# \tgrep \"^.GNUPG:.\" <expect.stderr.1 >expect.stderr &&\n     ++# \tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n     ++# \tgrep \"^.GNUPG:.\" <actual.stderr.1 >actual.stderr &&\n     ++# \ttest_cmp expect.stdout actual.stdout &&\n     ++# \ttest_cmp expect.stderr actual.stderr\n     ++# '\n     ++\n     ++# test_expect_success GPGSM 'verify multiple tags x509' '\n     ++#\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++# \ttags=\"seventh-signed ninth-signed-x509\" &&\n     ++# \tfor i in $tags\n     ++# \tdo\n     ++# \t\tgit verify-tag -v --raw $i || return 1\n     ++# \tdone >expect.stdout 2>expect.stderr.1 &&\n     ++# \tgrep \"^.GNUPG:.\" <expect.stderr.1 >expect.stderr &&\n     ++# \tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n     ++# \tgrep \"^.GNUPG:.\" <actual.stderr.1 >actual.stderr &&\n     ++# \ttest_cmp expect.stdout actual.stdout &&\n     ++# \ttest_cmp expect.stderr actual.stderr\n     ++# '\n     ++\n     ++test_expect_success GPGSSH 'verifying tag with --format' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tcat >expect <<-\\EOF &&\n     ++\ttagname : fourth-signed\n     ++\tEOF\n     ++\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently' '\n     ++\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n     ++\ttest_must_be_empty actual-forged\n     ++'\n     ++\n     ++test_done\n     +\n     + ## t/t7527-signed-commit-ssh.sh (new) ##\n     +@@\n     ++#!/bin/sh\n     ++\n     ++test_description='ssh signed commit tests'\n     ++GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n     ++export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     ++\n     ++. ./test-lib.sh\n     ++GNUPGHOME_NOT_USED=$GNUPGHOME\n     ++. \"$TEST_DIRECTORY/lib-gpg.sh\"\n     ++\n     ++test_expect_success GPGSSH 'create signed commits' '\n     ++\ttest_oid_cache <<-\\EOF &&\n     ++\theader sha1:gpgsig\n     ++\theader sha256:gpgsig-sha256\n     ++\tEOF\n     ++\n     ++\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n     ++\ttest_config gpg.format ssh &&\n     ++\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\n     ++\techo 1 >file && git add file &&\n     ++\ttest_tick && git commit -S -m initial &&\n     ++\tgit tag initial &&\n     ++\tgit branch side &&\n     ++\n     ++\techo 2 >file && test_tick && git commit -a -S -m second &&\n     ++\tgit tag second &&\n     ++\n     ++\tgit checkout side &&\n     ++\techo 3 >elif && git add elif &&\n     ++\ttest_tick && git commit -m \"third on side\" &&\n     ++\n     ++\tgit checkout main &&\n     ++\ttest_tick && git merge -S side &&\n     ++\tgit tag merge &&\n     ++\n     ++\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n     ++\tgit tag fourth-unsigned &&\n     ++\n     ++\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n     ++\tgit tag fourth-signed &&\n     ++\n     ++\tgit config commit.gpgsign true &&\n     ++\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n     ++\tgit tag fifth-signed &&\n     ++\n     ++\tgit config commit.gpgsign false &&\n     ++\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n     ++\tgit tag sixth-unsigned &&\n     ++\n     ++\tgit config commit.gpgsign true &&\n     ++\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n     ++\tgit tag seventh-unsigned &&\n     ++\n     ++\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n     ++\tgit tag seventh-signed &&\n     ++\n     ++\techo 8 >file && test_tick && git commit -a -m eighth -S\"${SIGNING_KEY_UNTRUSTED}\" &&\n     ++\tgit tag eighth-signed-alt &&\n     ++\n     ++\t# commit.gpgsign is still on but this must not be signed\n     ++\techo 9 | git commit-tree HEAD^{tree} >oid &&\n     ++\ttest_line_count = 1 oid &&\n     ++\tgit tag ninth-unsigned $(cat oid) &&\n     ++\t# explicit -S of course must sign.\n     ++\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n     ++\ttest_line_count = 1 oid &&\n     ++\tgit tag tenth-signed $(cat oid) &&\n     ++\n     ++\t# --gpg-sign[=<key-id>] must sign.\n     ++\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n     ++\ttest_line_count = 1 oid &&\n     ++\tgit tag eleventh-signed $(cat oid) &&\n     ++\techo 12 | git commit-tree --gpg-sign=\"${SIGNING_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n     ++\ttest_line_count = 1 oid &&\n     ++\tgit tag twelfth-signed-alt $(cat oid)\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'verify and show signatures' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.mintrustlevel UNDEFINED &&\n     ++\t(\n     ++\t\tfor commit in initial second merge fourth-signed \\\n     ++\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n     ++\t\t\televenth-signed\n     ++\t\tdo\n     ++\t\t\tgit verify-commit $commit &&\n     ++\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n     ++\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\techo $commit OK || exit 1\n     ++\t\tdone\n     ++\t) &&\n     ++\t(\n     ++\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n     ++\t\t\tseventh-unsigned ninth-unsigned\n     ++\t\tdo\n     ++\t\t\ttest_must_fail git verify-commit $commit &&\n     ++\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n     ++\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\techo $commit OK || exit 1\n     ++\t\tdone\n     ++\t) &&\n     ++\t(\n     ++\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n     ++\t\tdo\n     ++\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n     ++\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n     ++\t\t\techo $commit OK || exit 1\n     ++\t\tdone\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit verify-commit eighth-signed-alt 2>actual &&\n     ++\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\tgrep \"${KEY_NOT_TRUSTED}\" actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.minTrustLevel fully &&\n     ++\tgit verify-commit sixth-signed\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.minTrustLevel marginal &&\n     ++\tgit verify-commit sixth-signed\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n     ++\ttest_config gpg.minTrustLevel ultimate &&\n     ++\ttest_must_fail git verify-commit eighth-signed-alt\n     ++'\n     ++\n     ++# test_expect_success GPGSSH 'verify signatures with --raw' '\n     ++# \t(\n     ++# \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n     ++# \t\tdo\n     ++# \t\t\tgit verify-commit --raw $commit 2>actual &&\n     ++# \t\t\tgrep \"GOODSIG\" actual &&\n     ++# \t\t\t! grep \"BADSIG\" actual &&\n     ++# \t\t\techo $commit OK || exit 1\n     ++# \t\tdone\n     ++# \t) &&\n     ++# \t(\n     ++# \t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n     ++# \t\tdo\n     ++# \t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n     ++# \t\t\t! grep \"GOODSIG\" actual &&\n     ++# \t\t\t! grep \"BADSIG\" actual &&\n     ++# \t\t\techo $commit OK || exit 1\n     ++# \t\tdone\n     ++# \t) &&\n     ++# \t(\n     ++# \t\tfor commit in eighth-signed-alt\n     ++# \t\tdo\n     ++# \t\t\tgit verify-commit --raw $commit 2>actual &&\n     ++# \t\t\tgrep \"GOODSIG\" actual &&\n     ++# \t\t\t! grep \"BADSIG\" actual &&\n     ++# \t\t\tgrep \"TRUST_UNDEFINED\" actual &&\n     ++# \t\t\techo $commit OK || exit 1\n     ++# \t\tdone\n     ++# \t)\n     ++# '\n     ++\n     ++test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n     ++\tgit cat-file commit fourth-signed >output &&\n     ++\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'show signed commit with signature' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit show -s initial >commit &&\n     ++\tgit show -s --show-signature initial >show &&\n     ++\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n     ++\tgit cat-file commit initial >cat &&\n     ++\tgrep -v -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n     ++\tgrep -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n     ++\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n     ++\ttest_cmp show.commit commit &&\n     ++\ttest_cmp show.gpg verify.2 &&\n     ++\ttest_cmp cat.commit verify.1\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'detect fudged signature' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit cat-file commit seventh-signed >raw &&\n     ++\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n     ++\tgit hash-object -w -t commit forged1 >forged1.commit &&\n     ++\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n     ++\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n     ++\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n     ++\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n     ++\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'detect fudged signature with NUL' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit cat-file commit seventh-signed >raw &&\n     ++\tcat raw >forged2 &&\n     ++\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n     ++\tgit hash-object -w -t commit forged2 >forged2.commit &&\n     ++\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n     ++\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n     ++\tgrep \"${BAD_SIGNATURE}\" actual2 &&\n     ++\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual2\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'amending already signed commit' '\n     ++\ttest_config gpg.format ssh &&\n     ++\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit checkout fourth-signed^0 &&\n     ++\tgit commit --amend -S --no-edit &&\n     ++\tgit verify-commit HEAD &&\n     ++\tgit show -s --show-signature HEAD >actual &&\n     ++\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t! grep \"${BAD_SIGNATURE}\" actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'show good signature with custom format' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\tcat >expect.tmpl <<-\\EOF &&\n     ++\tG\n     ++\tFINGERPRINT\n     ++\tprincipal_1\n     ++\tFINGERPRINT\n     ++\t\n     ++\tEOF\n     ++\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n     ++\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'show bad signature with custom format' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tcat >expect <<-\\EOF &&\n     ++\tB\n     ++\n     ++\n     ++\n     ++\n     ++\tEOF\n     ++\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'show untrusted signature with custom format' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tcat >expect.tmpl <<-\\EOF &&\n     ++\tU\n     ++\tFINGERPRINT\n     ++\n     ++\tFINGERPRINT\n     ++\t\n     ++\tEOF\n     ++\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n     ++\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tcat >expect.tmpl <<-\\EOF &&\n     ++\tundefined\n     ++\tFINGERPRINT\n     ++\n     ++\tFINGERPRINT\n     ++\n     ++\tEOF\n     ++\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n     ++\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tcat >expect.tmpl <<-\\EOF &&\n     ++\tfully\n     ++\tFINGERPRINT\n     ++\tprincipal_1\n     ++\tFINGERPRINT\n     ++\n     ++\tEOF\n     ++\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'show lack of signature with custom format' '\n     ++\tcat >expect <<-\\EOF &&\n     ++\tN\n     ++\n     ++\n     ++\n     ++\n     ++\tEOF\n     ++\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config log.showsignature true &&\n     ++\tgit show initial >actual &&\n     ++\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n     ++'\n     ++\n     ++test_expect_success GPGSSH 'check config gpg.format values' '\n     ++\ttest_config gpg.format ssh &&\n     ++\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\ttest_config gpg.format ssh &&\n     ++\tgit commit -S --amend -m \"success\" &&\n     ++\ttest_config gpg.format OpEnPgP &&\n     ++\ttest_must_fail git commit -S --amend -m \"fail\"\n     ++'\n     ++\n     ++# test_expect_success GPGSSH 'detect fudged commit with double signature' '\n     ++# \tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n     ++# \tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n     ++# \t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n     ++# \tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n     ++# \tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n     ++# \tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n     ++# \t\tdouble-combined.asc > double-gpgsig &&\n     ++# \tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n     ++# \tgit hash-object -w -t commit double-commit >double-commit.commit &&\n     ++# \ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n     ++# \tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n     ++# \tgrep \"BAD signature from\" double-actual &&\n     ++# \tgrep \"Good signature from\" double-actual\n     ++# '\n     ++\n     ++# test_expect_success GPGSSH 'show double signature with custom format' '\n     ++# \tcat >expect <<-\\EOF &&\n     ++# \tE\n     ++\n     ++\n     ++\n     ++\n     ++# \tEOF\n     ++# \tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n     ++# \ttest_cmp expect actual\n     ++# '\n     ++\n     ++\n     ++# test_expect_success GPGSSH 'verify-commit verifies multiply signed commits' '\n     ++# \tgit init multiply-signed &&\n     ++# \tcd multiply-signed &&\n     ++# \ttest_commit first &&\n     ++# \techo 1 >second &&\n     ++# \tgit add second &&\n     ++# \ttree=$(git write-tree) &&\n     ++# \tparent=$(git rev-parse HEAD^{commit}) &&\n     ++# \tgit commit --gpg-sign -m second &&\n     ++# \tgit cat-file commit HEAD &&\n     ++# \t# Avoid trailing whitespace.\n     ++# \tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n     ++# \tQtree $tree\n     ++# \tQparent $parent\n     ++# \tQauthor A U Thor <author@example.com> 1112912653 -0700\n     ++# \tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n     ++# \tQgpgsig -----BEGIN PGP SIGNATURE-----\n     ++# \tQZ\n     ++# \tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n     ++# \tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n     ++# \tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n     ++# \tQ =tQ0N\n     ++# \tQ -----END PGP SIGNATURE-----\n     ++# \tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n     ++# \tQZ\n     ++# \tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n     ++# \tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n     ++# \tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n     ++# \tQ =pIwP\n     ++# \tQ -----END PGP SIGNATURE-----\n     ++# \tQ\n     ++# \tQsecond\n     ++# \tEOF\n     ++# \thead=$(git hash-object -t commit -w commit) &&\n     ++# \tgit reset --hard $head &&\n     ++# \tgit verify-commit $head 2>actual &&\n     ++# \tgrep \"Good signature from\" actual &&\n     ++# \t! grep \"BAD signature from\" actual\n     ++# '\n     ++\n     ++test_done\n\n\n Documentation/config/gpg.txt     |  35 ++-\n Documentation/config/user.txt    |   6 +\n builtin/receive-pack.c           |   4 +-\n fmt-merge-msg.c                  |   4 +-\n gpg-interface.c                  | 414 +++++++++++++++++++++++++++----\n gpg-interface.h                  |  12 +-\n log-tree.c                       |   8 +-\n pretty.c                         |   4 +-\n send-pack.c                      |   6 +-\n t/lib-gpg.sh                     |  27 ++\n t/t4202-log.sh                   |  17 ++\n t/t5534-push-signed.sh           | 102 ++++++++\n t/t7031-verify-tag-signed-ssh.sh | 176 +++++++++++++\n t/t7527-signed-commit-ssh.sh     | 398 +++++++++++++++++++++++++++++\n 14 files changed, 1147 insertions(+), 66 deletions(-)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n create mode 100755 t/t7527-signed-commit-ssh.sh\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..c2bc4d06a66 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\n@@ -33,3 +33,34 @@ gpg.minTrustLevel::\n * `marginal`\n * `fully`\n * `ultimate`\n+\n+gpg.ssh.keyring::\n+\tA file containing all valid SSH public signing keys. \n+\tSimilar to an .ssh/authorized_keys file.\n+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n+\tIf a signing key is found in this file then the trust level will\n+\tbe set to \"fully\". Otherwise if the key is not present\n+\tbut the signature is still valid then the trust level will be \"undefined\".\n+\n+\tThis file can be set to a location outside of the repository\n+\tand every developer maintains their own trust store.\n+\tA central repository server could generate this file automatically\n+\tfrom ssh keys with push\taccess to verify the code against.\n+\tIn a corporate setting this file is probably generated at a global location\n+\tfrom some automation that already handles developer ssh keys. \n+\t\n+\tA repository that is only allowing signed commits can store the file \n+\tin the repository itself using a relative path. This way only committers\n+\twith an already valid key can add or change keys in the keyring.\n+\n+\tUsing a SSH CA key with the cert-authority option \n+\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n+\n+\tTo revoke a key place the public key without the principal into the \n+\trevocationKeyring.\n+\n+gpg.ssh.revocationKeyring::\n+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n+\tSee ssh-keygen(1) for details.\n+\tIf a public key is found in this file then it will always be treated\n+\tas having trust level \"never\" and signatures will show as invalid.\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..e71a099b8b8 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,9 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and \n+\tcorresponds to the private key used for signing. The private key \n+\tneeds to be available via ssh-agent. Alternatively it can be set to\n+\ta file containing a private key directly. If not set git will call \n+\t\"ssh-add -L\" and try to use the first key available.\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513ac..fd790f7fd72 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n {\n \tint status = parse_hide_refs_config(var, value, \"receive\");\n \n+\tgit_gpg_config(var, value, NULL);\n+\n \tif (status)\n \t\treturn status;\n \n@@ -767,7 +769,7 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tbogs = parse_signed_buffer(push_cert.buf, push_cert.len);\n \t\tcheck_signature(push_cert.buf, bogs, push_cert.buf + bogs,\n \t\t\t\tpush_cert.len - bogs, &sigcheck);\n-\n+\t\t\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 0f66818e0f8..1d7b64fa021 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tlen = payload.len;\n \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n \t\t\t\t\t sig.len, &sigc) &&\n-\t\t\t\t!sigc.gpg_output)\n+\t\t\t\t!sigc.output)\n \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n \t\t\telse\n-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n \t\t}\n \t\tsignature_check_clear(&sigc);\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..3b1a350bcfd 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -3,11 +3,13 @@\n #include \"config.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n+#include \"dir.h\"\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+const char *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -35,6 +37,14 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = {\n+\tNULL\n+};\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static struct gpg_format gpg_format[] = {\n \t{ .name = \"openpgp\", .program = \"gpg\",\n \t  .verify_args = openpgp_verify_args,\n@@ -44,6 +54,9 @@ static struct gpg_format gpg_format[] = {\n \t  .verify_args = x509_verify_args,\n \t  .sigs = x509_sigs\n \t},\n+\t{ .name = \"ssh\", .program = \"ssh-keygen\",\n+\t  .verify_args = ssh_verify_args,\n+\t  .sigs = ssh_sigs },\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -72,6 +85,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\n+\tFREE_AND_NULL(sigc->output);\n \tFREE_AND_NULL(sigc->gpg_output);\n \tFREE_AND_NULL(sigc->gpg_status);\n \tFREE_AND_NULL(sigc->signer);\n@@ -144,6 +158,53 @@ static int parse_gpg_trust_level(const char *level,\n \treturn 1;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tconst char *output = NULL;\n+\tchar *next = NULL;\n+\n+\t/* ssh-keysign output should be:\n+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\t * or for valid but unknown keys:\n+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n+\t */\n+\n+\toutput = xmemdupz(sigc->output, strcspn(sigc->output, \" \\n\"));\n+\tif (skip_prefix(sigc->output, \"Good \\\"git\\\" signature for \", &output)) {\n+\t\t// Valid signature for a trusted signer\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_FULLY;\n+\n+\t\tnext = strchrnul(output, ' '); // 'principal'\n+\t\treplace_cstring(&sigc->signer, output, next);\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // 'with'\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // KEY Type\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // 'key'\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, '\\n'); // key\n+\t\treplace_cstring(&sigc->fingerprint, output, next);\n+\t\treplace_cstring(&sigc->key, output, next);\n+\t} else if (skip_prefix(sigc->output, \"Good \\\"git\\\" signature with \", &output)) {\n+\t\t// Valid signature, but key unknown\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_UNDEFINED;\n+\n+\t\tnext = strchrnul(output, ' '); // KEY Type\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, ' '); // 'key'\n+\t\toutput = next + 1;\n+\t\tnext = strchrnul(output, '\\n'); // key\n+\t\treplace_cstring(&sigc->fingerprint, output, next);\n+\t\treplace_cstring(&sigc->key, output, next);\n+\t} else {\n+\t\tsigc->result = 'B';\n+\t\tsigc->trust_level = TRUST_NEVER;\n+\t}\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n@@ -257,16 +318,18 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n-static int verify_signed_buffer(const char *payload, size_t payload_size,\n-\t\t\t\tconst char *signature, size_t signature_size,\n-\t\t\t\tstruct strbuf *gpg_output,\n-\t\t\t\tstruct strbuf *gpg_status)\n+static int verify_ssh_signature(struct signature_check *sigc, struct gpg_format *fmt,\n+\tconst char *payload, size_t payload_size,\n+\tconst char *signature, size_t signature_size)\n {\n-\tstruct child_process gpg = CHILD_PROCESS_INIT;\n-\tstruct gpg_format *fmt;\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -279,29 +342,125 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\treturn -1;\n \t}\n \n-\tfmt = get_format_by_sig(signature);\n-\tif (!fmt)\n-\t\tBUG(\"bad signature '%s'\", signature);\n+\t// Find the principal from the  signers\n+\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n+\t\t\t\t\t\"-Y\", \"find-principals\",\n+\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n+\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\tNULL);\n+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\tif (strstr(ssh_keygen_err.buf, \"unknown option\")) {\n+\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n+\t}\n+\tif (ret || !ssh_keygen_out.len) {\n+\t\t// We did not find a matching principal in the keyring - Check without validation\n+\t\tchild_process_init(&ssh_keygen);\n+\t\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n+\t\t\t\t\t\t\"-Y\", \"check-novalidate\",\n+\t\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\t\tNULL);\n+\t\tret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t} else {\n+\t\t// Check every principal we found (one per line)\n+\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n+\n+\t\t\ttrust_size = strcspn(line, \" \\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n+\n+\t\t\tchild_process_init(&ssh_keygen);\n+\t\t\tstrbuf_release(&ssh_keygen_out);\n+\t\t\tstrbuf_release(&ssh_keygen_err);\n+\t\t\tstrvec_push(&ssh_keygen.args,fmt->program);\n+\t\t\t// We found principals - Try with each until we find a match\n+\t\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"verify\",\n+\t\t\t\t\t\t\t//TODO: sprintf(\"-Overify-time=%s\", commit->date...),\n+\t\t\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n+\t\t\t\t\t\t\t\"-I\", principal,\n+\t\t\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\t\t\tNULL);\n+\n+\t\t\tif (ssh_revocation_file && file_exists(ssh_revocation_file)) {\n+\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\", ssh_revocation_file, NULL);\n+\t\t\t}\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t\t&ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n+\n+\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n+\t\t\tif (ret == 0)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n+\n+\t//sigc->gpg_output = strbuf_detach(&ssh_keygen_err, NULL); // This flip around is broken...\n+\tsigc->gpg_status = strbuf_detach(&ssh_keygen_out, NULL);\n+\n+\tparse_ssh_output(sigc);\n+\n+\tdelete_tempfile(&temp);\n+\tstrbuf_release(&ssh_keygen_out);\n+\tstrbuf_release(&ssh_keygen_err);\n+\n+\treturn ret;\n+}\n+\n+static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt, \n+\tconst char *payload, size_t payload_size,\n+\tconst char *signature, size_t signature_size)\n+{\n+\tstruct child_process gpg = CHILD_PROCESS_INIT;\n+\tstruct tempfile *temp;\n+\tint ret;\n+\tstruct strbuf gpg_out = STRBUF_INIT;\n+\tstruct strbuf gpg_err = STRBUF_INIT;\n+\n+\ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n+\tif (!temp)\n+\t\treturn error_errno(_(\"could not create temporary file\"));\n+\tif (write_in_full(temp->fd, signature, signature_size) < 0 ||\n+\t    close_tempfile_gently(temp) < 0) {\n+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n+\t\t\t    temp->filename.buf);\n+\t\tdelete_tempfile(&temp);\n+\t\treturn -1;\n+\t}\n \n \tstrvec_push(&gpg.args, fmt->program);\n \tstrvec_pushv(&gpg.args, fmt->verify_args);\n \tstrvec_pushl(&gpg.args,\n-\t\t     \"--status-fd=1\",\n-\t\t     \"--verify\", temp->filename.buf, \"-\",\n-\t\t     NULL);\n-\n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n+\t\t\t\"--status-fd=1\",\n+\t\t\t\"--verify\", temp->filename.buf, \"-\",\n+\t\t\tNULL);\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n+\tret = pipe_command(&gpg, payload, payload_size, &gpg_out, 0,\n+\t\t\t\t&gpg_err, 0);\n \tsigchain_pop(SIGPIPE);\n+\tret |= !strstr(gpg_out.buf, \"\\n[GNUPG:] GOODSIG \");\n \n-\tdelete_tempfile(&temp);\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tsigc->output = strbuf_detach(&gpg_err, NULL);\n+\tsigc->gpg_status = strbuf_detach(&gpg_out, NULL);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n-\tstrbuf_release(&buf); /* no matter it was used or not */\n+\tparse_gpg_output(sigc);\n+\n+\tdelete_tempfile(&temp);\n+\tstrbuf_release(&gpg_out);\n+\tstrbuf_release(&gpg_err);\n \n \treturn ret;\n }\n@@ -309,35 +468,34 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n-\tstruct strbuf gpg_output = STRBUF_INIT;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct gpg_format *fmt;\n \tint status;\n \n \tsigc->result = 'N';\n \tsigc->trust_level = -1;\n \n-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n-\t\t\t\t      &gpg_output, &gpg_status);\n-\tif (status && !gpg_output.len)\n-\t\tgoto out;\n-\tsigc->payload = xmemdupz(payload, plen);\n-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tfmt = get_format_by_sig(signature);\n+\tif (!fmt)\n+\t\tBUG(\"bad signature '%s'\", signature);\n+\n+\tif (!strcmp(fmt->name, \"ssh\")) {\n+\t\tstatus = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);\n+\t} else {\n+\t\tstatus = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);\n+\t}\n+\tif (status && !sigc->gpg_output)\n+\t\treturn !!status;\n+\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n- out:\n-\tstrbuf_release(&gpg_status);\n-\tstrbuf_release(&gpg_output);\n-\n \treturn !!status;\n }\n \n void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n {\n \tconst char *output = flags & GPG_VERIFY_RAW ?\n-\t\tsigc->gpg_status : sigc->gpg_output;\n+\t\tsigc->gpg_status : sigc->output;\n \n \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n \t\tfputs(sigc->payload, stdout);\n@@ -388,12 +546,31 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tint ret;\n \n \tif (!strcmp(var, \"user.signingkey\")) {\n+\t\t/* user.signingkey can contain one of the following\n+\t\t * when format = openpgp/x509\n+\t\t *   - GPG KeyID\n+\t\t * when format = ssh\n+\t\t *   - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)\n+\t\t *   - path to a file containing a public or a private ssh key\n+\t\t */\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n \t\tset_signing_key(value);\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.keyring\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationkeyring\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.format\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n@@ -425,6 +602,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -433,11 +613,80 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+static char *get_ssh_key_fingerprint(const char *signing_key) {\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n+\tstruct strbuf **fingerprint;\n+\n+\t/* For SSH Signing this can contain a filename or a public key\n+\t* For textual representation we usually want a fingerprint\n+\t*/\n+\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n+\t\t\t\t\t\"-lf\", \"-\",\n+\t\t\t\t\tNULL);\n+\t\tret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0,  NULL, 0);\n+\t} else {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n+\t\t\t\t\t\"-lf\", configured_signing_key,\n+\t\t\t\t\tNULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);\n+\t\tif (!!ret)\n+\t\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n+\t\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n+\t\tif (fingerprint[1]) {\n+\t\t\treturn strbuf_detach(fingerprint[1], NULL);\n+\t\t}\n+\t}\n+\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n+}\n+\n+// Returns the first public key from an ssh-agent to use for signing\n+static char *get_default_ssh_signing_key(void) {\n+\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf key_stdout = STRBUF_INIT;\n+\tstruct strbuf **keys;\n+\n+\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n+\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n+\tif (!ret) { \n+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n+\t\tif (keys[0])\n+\t\t\treturn strbuf_detach(keys[0], NULL);\n+\t}\n+\n+\treturn \"\";\n+}\n+\n+// Returns a textual but unique representation ot the signing key\n+const char *get_signing_key_id(void) {\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_ssh_key_fingerprint(get_signing_key());\n+\t} else {\n+\t\t// GPG/GPGSM only store a key id on this variable\n+\t\treturn get_signing_key();\n+\t}\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_default_ssh_signing_key();\n+\t} else {\n+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n+\t}\n+}\n+\n+const char *get_ssh_allowed_signers(void)\n+{\n+\tif (ssh_allowed_signers)\n+\t\treturn ssh_allowed_signers;\n+\n+\tdie(\"A Path to an allowed signers ssh keyring is needed for validation\");\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n@@ -446,25 +695,88 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \tint ret;\n \tsize_t i, j, bottom;\n \tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n+\tchar *ssh_signing_key_file = NULL;\n+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n+\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\tif (!signing_key || signing_key[0] == '\\0')\n+\t\t\treturn error(_(\"user.signingkey needs to be set for ssh signing\"));\n+\n+\n+\t\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\t\t// A literal ssh key\n+\t\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\t\tif (!temp)\n+\t\t\t\treturn error_errno(_(\"could not create temporary file\"));\n+\t\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) < 0 ||\n+\t\t\t\tclose_tempfile_gently(temp) < 0) {\n+\t\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"), temp->filename.buf);\n+\t\t\t\tdelete_tempfile(&temp);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t\tssh_signing_key_file= temp->filename.buf;\n+\t\t} else {\n+\t\t\t// We assume a file\n+\t\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n+\t\t}\n \n-\tstrvec_pushl(&gpg.args,\n-\t\t     use_format->program,\n-\t\t     \"--status-fd=2\",\n-\t\t     \"-bsau\", signing_key,\n-\t\t     NULL);\n+\t\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n+\t\tif (!buffer_file)\n+\t\t\treturn error_errno(_(\"could not create temporary file\"));\n+\t\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n+\t\t\tclose_tempfile_gently(buffer_file) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"), buffer_file->filename.buf);\n+\t\t\tdelete_tempfile(&buffer_file);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tstrvec_pushl(&gpg.args, use_format->program ,\n+\t\t\t\t\t\"-Y\", \"sign\",\n+\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\"-f\", ssh_signing_key_file,\n+\t\t\t\t\tbuffer_file->filename.buf,\n+\t\t\t\t\tNULL);\n+\n+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\tret = pipe_command(&gpg, NULL, 0, NULL, 0, &gpg_status, 0);\n+\t\tsigchain_pop(SIGPIPE);\n+\n+\t\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n+\t\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n+\t\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n+\t\t\terror_errno(_(\"failed reading ssh signing data buffer from '%s'\"), ssh_signature_filename.buf);\n+\t\t}\n+\t\tunlink_or_warn(ssh_signature_filename.buf);\n+\t\tstrbuf_release(&ssh_signature_filename);\n+\t\tdelete_tempfile(&buffer_file);\n+\t} else {\n+\t\tstrvec_pushl(&gpg.args, use_format->program ,\n+\t\t\t\t\t\"--status-fd=2\",\n+\t\t\t\t\t\"-bsau\", signing_key,\n+\t\t\t\t\tNULL);\n+\n+\t\t/*\n+\t\t* When the username signingkey is bad, program could be terminated\n+\t\t* because gpg exits without reading and then write gets SIGPIPE.\n+\t\t*/\n+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\tret = pipe_command(&gpg, buffer->buf, buffer->len, signature, 1024, &gpg_status, 0);\n+\t\tsigchain_pop(SIGPIPE);\n+\t}\n \n \tbottom = signature->len;\n \n-\t/*\n-\t * When the username signingkey is bad, program could be terminated\n-\t * because gpg exits without reading and then write gets SIGPIPE.\n-\t */\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, buffer->buf, buffer->len,\n-\t\t\t   signature, 1024, &gpg_status, 0);\n-\tsigchain_pop(SIGPIPE);\n+\tif (temp)\n+\t\tdelete_tempfile(&temp);\n \n-\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\tif (strstr(gpg_status.buf, \"unknown option\")) {\n+\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n+\t\t}\n+\t} else {\n+\t\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n+\t}\n \tstrbuf_release(&gpg_status);\n \tif (ret)\n \t\treturn error(_(\"gpg failed to sign the data\"));\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..03d56475b56 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -17,8 +17,9 @@ enum signature_trust_level {\n \n struct signature_check {\n \tchar *payload;\n-\tchar *gpg_output;\n-\tchar *gpg_status;\n+\tchar *output;\n+\tchar *gpg_output; // This will be printed in commit logs\n+\tchar *gpg_status; // Only used internally -> remove\n \n \t/*\n \t * possible \"result\":\n@@ -64,6 +65,13 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+\n+/* Returns a textual unique representation of the signing key in use\n+ * Either a GPG KeyID or a SSH Key Fingerprint\n+ */\n+const char *get_signing_key_id(void);\n+\n+const char *get_ssh_allowed_signers(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\ndiff --git a/log-tree.c b/log-tree.c\nindex 7b823786c2c..20af9bd1c82 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit)\n \n \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n \t\t\t\t signature.len, &sigc);\n-\tif (status && !sigc.gpg_output)\n+\tif (status && !sigc.output)\n \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n \telse\n-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n+\t\tshow_sig_lines(opt, status, sigc.output);\n \tsignature_check_clear(&sigc);\n \n  out:\n@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,\n \t\t/* could have a good signature */\n \t\tstatus = check_signature(payload.buf, payload.len,\n \t\t\t\t\t signature.buf, signature.len, &sigc);\n-\t\tif (sigc.gpg_output)\n-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n+\t\tif (sigc.output)\n+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n \t\telse\n \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n \t\tsignature_check_clear(&sigc);\ndiff --git a/pretty.c b/pretty.c\nindex b1ecd039cef..daa71394efd 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n \t\tswitch (placeholder[1]) {\n \t\tcase 'G':\n-\t\t\tif (c->signature_check.gpg_output)\n-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n+\t\t\tif (c->signature_check.output)\n+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n \t\t\tbreak;\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\ndiff --git a/send-pack.c b/send-pack.c\nindex 9cb9f716509..c8fb0c30f87 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -342,12 +342,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tconst struct ref *ref;\n \tstruct string_list_item *item;\n \tchar *signing_key = xstrdup(get_signing_key());\n+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n \tconst char *cp, *np;\n \tstruct strbuf cert = STRBUF_INIT;\n \tint update_seen = 0;\n-\n+\t\n \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n \tdatestamp(&cert);\n \tstrbuf_addch(&cert, '\\n');\n \tif (args->url && *args->url) {\n@@ -387,6 +388,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \n free_return:\n \tfree(signing_key);\n+\tfree(signing_key_id);\n \tstrbuf_release(&cert);\n \treturn update_seen;\n }\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\nindex 9fc5241228e..96935582262 100644\n--- a/t/lib-gpg.sh\n+++ b/t/lib-gpg.sh\n@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 '\n \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n '\n \n+test_lazy_prereq GPGSSH '\n+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n+\ttest $? != 127 || exit 1\n+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n+\ttest $? = 0 || exit 1; \n+\tmkdir -p \"${GNUPGHOME}\" &&\n+\tchmod 0700 \"${GNUPGHOME}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n+\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"principal_\\\" NR \\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n+\tcat \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n+'\n+\n+SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n+SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n+SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n+SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n+SIGNING_KEY_PASSPHRASE=\"super_secret\"\n+SIGNING_KEYRING=\"${GNUPGHOME}/ssh.all_valid.keyring\"\n+\n+GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n+GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n+KEY_NOT_TRUSTED=\"No principal matched\"\n+BAD_SIGNATURE=\"Signature verification failed\"\n+\n sanitize_pgp() {\n \tperl -ne '\n \t\t/^-----END PGP/ and $in_pgp = 0;\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 350cfa35936..84227066685 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' '\n \tgit commit -S -m signed_commit\n '\n \n+test_expect_success GPGSSH 'setup sshkey signed branch' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n+\tgit checkout -b signed-ssh main &&\n+\techo foo >foo &&\n+\tgit add foo &&\n+\tgit commit -S -m signed_commit\n+'\n+\n test_expect_success GPGSM 'log x509 fingerprint' '\n \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n@@ -1640,6 +1650,13 @@ test_expect_success GPGSM 'log --graph --show-signature x509' '\n \tgrep \"^| gpgsm: Good signature\" actual\n '\n \n+test_expect_success GPGSSH 'log ssh key fingerprint' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature for merged tag' '\n \ttest_when_finished \"git reset --hard && git checkout main\" &&\n \tgit checkout -b plain main &&\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex bba768f5ded..19b37e999b4 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal_1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n \t# First, invoke receive-pack with dummy input to obtain its preamble.\n \tprepare_dst &&\n@@ -276,6 +323,61 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n+\ttest_config gpg.format ssh &&\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config user.email hasnokey@nowhere.com &&\n+\ttest_config gpg.format ssh &&\n+\t\n+\ttest_config user.signingkey \"\" &&\n+\t(\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git push --signed dst noop ff +noff\n+\t) &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal_1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'failed atomic push does not execute GPG' '\n \tprepare_dst &&\n \tgit -C dst config receive.certnonceseed sekrit &&\ndiff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh\nnew file mode 100755\nindex 00000000000..d2b7a525584\n--- /dev/null\n+++ b/t/t7031-verify-tag-signed-ssh.sh\n@@ -0,0 +1,176 @@\n+#!/bin/sh\n+\n+test_description='signed tag tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed tags ssh' '\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -m initial &&\n+\tgit tag -s -m initial initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -m second &&\n+\tgit tag -s -m second second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag -s -m merge merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag -s -m fourth fourth-signed &&\n+\n+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n+\tgit tag fifth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag -a -m sixth sixth-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n+\tgit tag -m seventh -s seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth &&\n+\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify and show ssh signatures' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'detect fudged ssh signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit cat-file tag seventh-signed >raw &&\n+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+# test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n+# \ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+# \t(\n+# \t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+# \t\tdo\n+# \t\t\tgit verify-tag --raw $tag 2>actual &&\n+# \t\t\tgrep \"GOODSIG\" actual &&\n+# \t\t\t! grep \"BADSIG\" actual &&\n+# \t\t\techo $tag OK || exit 1\n+# \t\tdone\n+# \t) &&\n+# \t(\n+# \t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+# \t\tdo\n+# \t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+# \t\t\t! grep \"GOODSIG\" actual &&\n+# \t\t\t! grep \"BADSIG\" actual &&\n+# \t\t\techo $tag OK || exit 1\n+# \t\tdone\n+# \t) &&\n+# \t(\n+# \t\tfor tag in eighth-signed-alt\n+# \t\tdo\n+# \t\t\tgit verify-tag --raw $tag 2>actual &&\n+# \t\t\tgrep \"GOODSIG\" actual &&\n+# \t\t\t! grep \"BADSIG\" actual &&\n+# \t\t\tgrep \"TRUST_UNDEFINED\" actual &&\n+# \t\t\techo $tag OK || exit 1\n+# \t\tdone\n+# \t)\n+# '\n+\n+# test_expect_success GPGSM 'verify signatures with --raw x509' '\n+# \tgit verify-tag --raw ninth-signed-x509 2>actual &&\n+# \tgrep \"GOODSIG\" actual &&\n+# \t! grep \"BADSIG\" actual &&\n+# \techo ninth-signed-x509 OK\n+# '\n+\n+# test_expect_success GPGSSH 'verify multiple tags' '\n+# \ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+# \ttags=\"fourth-signed sixth-signed seventh-signed\" &&\n+# \tfor i in $tags\n+# \tdo\n+# \t\tgit verify-tag -v --raw $i || return 1\n+# \tdone >expect.stdout 2>expect.stderr.1 &&\n+# \tgrep \"^.GNUPG:.\" <expect.stderr.1 >expect.stderr &&\n+# \tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+# \tgrep \"^.GNUPG:.\" <actual.stderr.1 >actual.stderr &&\n+# \ttest_cmp expect.stdout actual.stdout &&\n+# \ttest_cmp expect.stderr actual.stderr\n+# '\n+\n+# test_expect_success GPGSM 'verify multiple tags x509' '\n+#\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+# \ttags=\"seventh-signed ninth-signed-x509\" &&\n+# \tfor i in $tags\n+# \tdo\n+# \t\tgit verify-tag -v --raw $i || return 1\n+# \tdone >expect.stdout 2>expect.stderr.1 &&\n+# \tgrep \"^.GNUPG:.\" <expect.stderr.1 >expect.stderr &&\n+# \tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+# \tgrep \"^.GNUPG:.\" <actual.stderr.1 >actual.stderr &&\n+# \ttest_cmp expect.stdout actual.stdout &&\n+# \ttest_cmp expect.stderr actual.stderr\n+# '\n+\n+test_expect_success GPGSSH 'verifying tag with --format' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect <<-\\EOF &&\n+\ttagname : fourth-signed\n+\tEOF\n+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently' '\n+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n+\ttest_must_be_empty actual-forged\n+'\n+\n+test_done\ndiff --git a/t/t7527-signed-commit-ssh.sh b/t/t7527-signed-commit-ssh.sh\nnew file mode 100755\nindex 00000000000..f397a6dd327\n--- /dev/null\n+++ b/t/t7527-signed-commit-ssh.sh\n@@ -0,0 +1,398 @@\n+#!/bin/sh\n+\n+test_description='ssh signed commit tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed commits' '\n+\ttest_oid_cache <<-\\EOF &&\n+\theader sha1:gpgsig\n+\theader sha256:gpgsig-sha256\n+\tEOF\n+\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -S -m initial &&\n+\tgit tag initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -S -m second &&\n+\tgit tag second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n+\tgit tag fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag fourth-signed &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n+\tgit tag fifth-signed &&\n+\n+\tgit config commit.gpgsign false &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag sixth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n+\tgit tag seventh-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n+\tgit tag seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${SIGNING_KEY_UNTRUSTED}\" &&\n+\tgit tag eighth-signed-alt &&\n+\n+\t# commit.gpgsign is still on but this must not be signed\n+\techo 9 | git commit-tree HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag ninth-unsigned $(cat oid) &&\n+\t# explicit -S of course must sign.\n+\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag tenth-signed $(cat oid) &&\n+\n+\t# --gpg-sign[=<key-id>] must sign.\n+\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag eleventh-signed $(cat oid) &&\n+\techo 12 | git commit-tree --gpg-sign=\"${SIGNING_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag twelfth-signed-alt $(cat oid)\n+'\n+\n+test_expect_success GPGSSH 'verify and show signatures' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed \\\n+\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n+\t\t\televenth-signed\n+\t\tdo\n+\t\t\tgit verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n+\t\t\tseventh-unsigned ninth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n+\t\tdo\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit verify-commit eighth-signed-alt 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\tgrep \"${KEY_NOT_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n+# test_expect_success GPGSSH 'verify signatures with --raw' '\n+# \t(\n+# \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n+# \t\tdo\n+# \t\t\tgit verify-commit --raw $commit 2>actual &&\n+# \t\t\tgrep \"GOODSIG\" actual &&\n+# \t\t\t! grep \"BADSIG\" actual &&\n+# \t\t\techo $commit OK || exit 1\n+# \t\tdone\n+# \t) &&\n+# \t(\n+# \t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n+# \t\tdo\n+# \t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+# \t\t\t! grep \"GOODSIG\" actual &&\n+# \t\t\t! grep \"BADSIG\" actual &&\n+# \t\t\techo $commit OK || exit 1\n+# \t\tdone\n+# \t) &&\n+# \t(\n+# \t\tfor commit in eighth-signed-alt\n+# \t\tdo\n+# \t\t\tgit verify-commit --raw $commit 2>actual &&\n+# \t\t\tgrep \"GOODSIG\" actual &&\n+# \t\t\t! grep \"BADSIG\" actual &&\n+# \t\t\tgrep \"TRUST_UNDEFINED\" actual &&\n+# \t\t\techo $commit OK || exit 1\n+# \t\tdone\n+# \t)\n+# '\n+\n+test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n+\tgit cat-file commit fourth-signed >output &&\n+\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n+'\n+\n+test_expect_success GPGSSH 'show signed commit with signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit show -s initial >commit &&\n+\tgit show -s --show-signature initial >show &&\n+\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n+\tgit cat-file commit initial >cat &&\n+\tgrep -v -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n+\tgrep -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n+\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n+\ttest_cmp show.commit commit &&\n+\ttest_cmp show.gpg verify.2 &&\n+\ttest_cmp cat.commit verify.1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t commit forged1 >forged1.commit &&\n+\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature with NUL' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tcat raw >forged2 &&\n+\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n+\tgit hash-object -w -t commit forged2 >forged2.commit &&\n+\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual2 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual2\n+'\n+\n+test_expect_success GPGSSH 'amending already signed commit' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit checkout fourth-signed^0 &&\n+\tgit commit --amend -S --no-edit &&\n+\tgit verify-commit HEAD &&\n+\tgit show -s --show-signature HEAD >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual\n+'\n+\n+test_expect_success GPGSSH 'show good signature with custom format' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tG\n+\tFINGERPRINT\n+\tprincipal_1\n+\tFINGERPRINT\n+\t\n+\tEOF\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show bad signature with custom format' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect <<-\\EOF &&\n+\tB\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with custom format' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tU\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\t\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tundefined\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tfully\n+\tFINGERPRINT\n+\tprincipal_1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show lack of signature with custom format' '\n+\tcat >expect <<-\\EOF &&\n+\tN\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config log.showsignature true &&\n+\tgit show initial >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'check config gpg.format values' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.format ssh &&\n+\tgit commit -S --amend -m \"success\" &&\n+\ttest_config gpg.format OpEnPgP &&\n+\ttest_must_fail git commit -S --amend -m \"fail\"\n+'\n+\n+# test_expect_success GPGSSH 'detect fudged commit with double signature' '\n+# \tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n+# \tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n+# \t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n+# \tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n+# \tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n+# \tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n+# \t\tdouble-combined.asc > double-gpgsig &&\n+# \tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n+# \tgit hash-object -w -t commit double-commit >double-commit.commit &&\n+# \ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n+# \tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n+# \tgrep \"BAD signature from\" double-actual &&\n+# \tgrep \"Good signature from\" double-actual\n+# '\n+\n+# test_expect_success GPGSSH 'show double signature with custom format' '\n+# \tcat >expect <<-\\EOF &&\n+# \tE\n+\n+\n+\n+\n+# \tEOF\n+# \tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n+# \ttest_cmp expect actual\n+# '\n+\n+\n+# test_expect_success GPGSSH 'verify-commit verifies multiply signed commits' '\n+# \tgit init multiply-signed &&\n+# \tcd multiply-signed &&\n+# \ttest_commit first &&\n+# \techo 1 >second &&\n+# \tgit add second &&\n+# \ttree=$(git write-tree) &&\n+# \tparent=$(git rev-parse HEAD^{commit}) &&\n+# \tgit commit --gpg-sign -m second &&\n+# \tgit cat-file commit HEAD &&\n+# \t# Avoid trailing whitespace.\n+# \tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n+# \tQtree $tree\n+# \tQparent $parent\n+# \tQauthor A U Thor <author@example.com> 1112912653 -0700\n+# \tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n+# \tQgpgsig -----BEGIN PGP SIGNATURE-----\n+# \tQZ\n+# \tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n+# \tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n+# \tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n+# \tQ =tQ0N\n+# \tQ -----END PGP SIGNATURE-----\n+# \tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n+# \tQZ\n+# \tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n+# \tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n+# \tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n+# \tQ =pIwP\n+# \tQ -----END PGP SIGNATURE-----\n+# \tQ\n+# \tQsecond\n+# \tEOF\n+# \thead=$(git hash-object -t commit -w commit) &&\n+# \tgit reset --hard $head &&\n+# \tgit verify-commit $head 2>actual &&\n+# \tgrep \"Good signature from\" actual &&\n+# \t! grep \"BAD signature from\" actual\n+# '\n+\n+test_done\n\nbase-commit: d486ca60a51c9cb1fe068803c3f540724e95e83a\n-- \ngitgitgadget\n"},{"id":"429780","messageId":"87y2ab30yr.fsf@evledraar.gmail.com","threadId":"56054","inReplyTo":"pull.1041.v2.git.git.1626092359713.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-12T16:55:02Z","receivedAt":"2021-07-12T17:18:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 12 2021, Fabian Stelzer via GitGitGadget wrote:\n\n>  gpg.format::\n>  \tSpecifies which key format to use when signing with `--gpg-sign`.\n> -\tDefault is \"openpgp\" and another possible value is \"x509\".\n> +\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n>  \n>  gpg.<format>.program::\n>  \tUse this to customize the program used for the signing format you\n>  \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n>  \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n> -\tvalue for `gpg.x509.program` is \"gpgsm\".\n> +\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n>  \n>  gpg.minTrustLevel::\n>  \tSpecifies a minimum trust level for signature verification.  If\n> @@ -33,3 +33,34 @@ gpg.minTrustLevel::\n>  * `marginal`\n>  * `fully`\n>  * `ultimate`\n> +\n> +gpg.ssh.keyring::\n> +\tA file containing all valid SSH public signing keys. \n> +\tSimilar to an .ssh/authorized_keys file.\n> +\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n> +\tIf a signing key is found in this file then the trust level will\n> +\tbe set to \"fully\". Otherwise if the key is not present\n> +\tbut the signature is still valid then the trust level will be \"undefined\".\n> +\n> +\tThis file can be set to a location outside of the repository\n> +\tand every developer maintains their own trust store.\n> +\tA central repository server could generate this file automatically\n> +\tfrom ssh keys with push\taccess to verify the code against.\n> +\tIn a corporate setting this file is probably generated at a global location\n> +\tfrom some automation that already handles developer ssh keys. \n> +\t\n> +\tA repository that is only allowing signed commits can store the file \n> +\tin the repository itself using a relative path. This way only committers\n> +\twith an already valid key can add or change keys in the keyring.\n> +\n> +\tUsing a SSH CA key with the cert-authority option \n> +\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n> +\n> +\tTo revoke a key place the public key without the principal into the \n> +\trevocationKeyring.\n> +\n> +gpg.ssh.revocationKeyring::\n> +\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n> +\tSee ssh-keygen(1) for details.\n> +\tIf a public key is found in this file then it will always be treated\n> +\tas having trust level \"never\" and signatures will show as invalid.\n> diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\n> index 59aec7c3aed..e71a099b8b8 100644\n> --- a/Documentation/config/user.txt\n> +++ b/Documentation/config/user.txt\n> @@ -36,3 +36,9 @@ user.signingKey::\n>  \tcommit, you can override the default selection with this variable.\n>  \tThis option is passed unchanged to gpg's --local-user parameter,\n>  \tso you may specify a key using any method that gpg supports.\n> +\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n> +\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and \n> +\tcorresponds to the private key used for signing. The private key \n> +\tneeds to be available via ssh-agent. Alternatively it can be set to\n> +\ta file containing a private key directly. If not set git will call \n> +\t\"ssh-add -L\" and try to use the first key available.\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index a34742513ac..fd790f7fd72 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n>  {\n>  \tint status = parse_hide_refs_config(var, value, \"receive\");\n>  \n> +\tgit_gpg_config(var, value, NULL);\n> +\n>  \tif (status)\n>  \t\treturn status;\n>  \n> @@ -767,7 +769,7 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n>  \t\tbogs = parse_signed_buffer(push_cert.buf, push_cert.len);\n>  \t\tcheck_signature(push_cert.buf, bogs, push_cert.buf + bogs,\n>  \t\t\t\tpush_cert.len - bogs, &sigcheck);\n> -\n> +\t\t\n\nStray whitespace change.\n\n> +static void parse_ssh_output(struct signature_check *sigc)\n> +{\n> +\tconst char *output = NULL;\n> +\tchar *next = NULL;\n> +\n> +\t/* ssh-keysign output should be:\n> +\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n> +\t * or for valid but unknown keys:\n> +\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n> +\t */\n\nStyle:\n\n /*\n  * Comments like this\n  */\n\nNot /* Comments [...]\n\n> +\n> +\toutput = xmemdupz(sigc->output, strcspn(sigc->output, \" \\n\"));\n> +\tif (skip_prefix(sigc->output, \"Good \\\"git\\\" signature for \", &output)) {\n> +\t\t// Valid signature for a trusted signer\n\nWe don't use C99 comments, so /* ... */ (but perhaps we should nowadays,\nbut that's another topic...).\n\n> +\t\tsigc->result = 'G';\n> +\t\tsigc->trust_level = TRUST_FULLY;\n> +\n> +\t\tnext = strchrnul(output, ' '); // 'principal'\n> +\t\treplace_cstring(&sigc->signer, output, next);\n> +\t\toutput = next + 1;\n> +\t\tnext = strchrnul(output, ' '); // 'with'\n> +\t\toutput = next + 1;\n> +\t\tnext = strchrnul(output, ' '); // KEY Type\n> +\t\toutput = next + 1;\n> +\t\tnext = strchrnul(output, ' '); // 'key'\n> +\t\toutput = next + 1;\n\nFWIW for new code we'd probably use string_list_split() or\nstring_list_split_in_place() or strbuf_split_buf() or something, but I\nsee this is following the existing pattern in the file...\n\n> +\t\tnext = strchrnul(output, '\\n'); // key\n>\n> +\t\treplace_cstring(&sigc->fingerprint, output, next);\n> +\t\treplace_cstring(&sigc->key, output, next);\n> +\t} else if (skip_prefix(sigc->output, \"Good \\\"git\\\" signature with \", &output)) {\n> +\t\t// Valid signature, but key unknown\n> +\t\tsigc->result = 'G';\n> +\t\tsigc->trust_level = TRUST_UNDEFINED;\n> +\n> +\t\tnext = strchrnul(output, ' '); // KEY Type\n> +\t\toutput = next + 1;\n> +\t\tnext = strchrnul(output, ' '); // 'key'\n> +\t\toutput = next + 1;\n> +\t\tnext = strchrnul(output, '\\n'); // key\n> +\t\treplace_cstring(&sigc->fingerprint, output, next);\n> +\t\treplace_cstring(&sigc->key, output, next);\n> +\t} else {\n> +\t\tsigc->result = 'B';\n> +\t\tsigc->trust_level = TRUST_NEVER;\n> +\t}\n> +}\n> +\n>  static void parse_gpg_output(struct signature_check *sigc)\n>  {\n>  \tconst char *buf = sigc->gpg_status;\n> @@ -257,16 +318,18 @@ error:\n>  \tFREE_AND_NULL(sigc->key);\n>  }\n>  \n> -static int verify_signed_buffer(const char *payload, size_t payload_size,\n> -\t\t\t\tconst char *signature, size_t signature_size,\n> -\t\t\t\tstruct strbuf *gpg_output,\n> -\t\t\t\tstruct strbuf *gpg_status)\n> +static int verify_ssh_signature(struct signature_check *sigc, struct gpg_format *fmt,\n\nWe usually wrap at 80 characters, so since you're wrapping anyway...\n\n> +\tconst char *payload, size_t payload_size,\n> +\tconst char *signature, size_t signature_size)\n>  {\n> -\tstruct child_process gpg = CHILD_PROCESS_INIT;\n> -\tstruct gpg_format *fmt;\n> +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n>  \tstruct tempfile *temp;\n>  \tint ret;\n> -\tstruct strbuf buf = STRBUF_INIT;\n> +\tconst char *line;\n> +\tsize_t trust_size;\n> +\tchar *principal;\n> +\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n> +\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n>  \n>  \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n>  \tif (!temp)\n> @@ -279,29 +342,125 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>  \t\treturn -1;\n>  \t}\n>  \n> -\tfmt = get_format_by_sig(signature);\n> -\tif (!fmt)\n> -\t\tBUG(\"bad signature '%s'\", signature);\n> +\t// Find the principal from the  signers\n> +\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n> +\t\t\t\t\t\"-Y\", \"find-principals\",\n> +\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n> +\t\t\t\t\t\"-s\", temp->filename.buf,\n> +\t\t\t\t\tNULL);\n> +\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n> +\tif (strstr(ssh_keygen_err.buf, \"unknown option\")) {\n> +\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n> +\t}\n> +\tif (ret || !ssh_keygen_out.len) {\n> +\t\t// We did not find a matching principal in the keyring - Check without validation\n> +\t\tchild_process_init(&ssh_keygen);\n> +\t\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n> +\t\t\t\t\t\t\"-Y\", \"check-novalidate\",\n> +\t\t\t\t\t\t\"-n\", \"git\",\n> +\t\t\t\t\t\t\"-s\", temp->filename.buf,\n> +\t\t\t\t\t\tNULL);\n> +\t\tret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n> +\t} else {\n> +\t\t// Check every principal we found (one per line)\n> +\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n\nHrm, can't we use strbuf_getline() here with the underlying io_pump API\nthat pipe_command() uses, instead of slurping it all up, and then\nsplitting on '\\n' ourselves? (I'm not sure)\n\n> +\t\t\twhile (*line == '\\n')\n> +\t\t\t\tline++;\n> +\t\t\tif (!*line)\n> +\t\t\t\tbreak;\n> +\n> +\t\t\ttrust_size = strcspn(line, \" \\n\");\n> +\t\t\tprincipal = xmemdupz(line, trust_size);\n> +\n> +\t\t\tchild_process_init(&ssh_keygen);\n> +\t\t\tstrbuf_release(&ssh_keygen_out);\n> +\t\t\tstrbuf_release(&ssh_keygen_err);\n> +\t\t\tstrvec_push(&ssh_keygen.args,fmt->program);\n> +\t\t\t// We found principals - Try with each until we find a match\n> +\t\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"verify\",\n> +\t\t\t\t\t\t\t//TODO: sprintf(\"-Overify-time=%s\", commit->date...),\n> +\t\t\t\t\t\t\t\"-n\", \"git\",\n> +\t\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n> +\t\t\t\t\t\t\t\"-I\", principal,\n> +\t\t\t\t\t\t\t\"-s\", temp->filename.buf,\n> +\t\t\t\t\t\t\tNULL);\n> +\n> +\t\t\tif (ssh_revocation_file && file_exists(ssh_revocation_file)) {\n> +\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\", ssh_revocation_file, NULL);\n\nDo we want to silently ignore missing but configured revocation files?\n\n> +\t\t\t}\n> +\n> +\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n> +\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n> +\t\t\t\t\t&ssh_keygen_out, 0, &ssh_keygen_err, 0);\n> +\t\t\tsigchain_pop(SIGPIPE);\n> +\n> +\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n> +\t\t\tif (ret == 0)\n> +\t\t\t\tbreak;\n> +\t\t}\n> +\t}\n> +\n> +\tsigc->payload = xmemdupz(payload, payload_size);\n> +\tstrbuf_stripspace(&ssh_keygen_out, 0);\n> +\tstrbuf_stripspace(&ssh_keygen_err, 0);\n> +\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n> +\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n> +\n> +\t//sigc->gpg_output = strbuf_detach(&ssh_keygen_err, NULL); // This flip around is broken...\n\nBroken how? And why the commented-out code as part of the patch?\n\n> -\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n> -\t\t\t\t      &gpg_output, &gpg_status);\n> -\tif (status && !gpg_output.len)\n> -\t\tgoto out;\n> -\tsigc->payload = xmemdupz(payload, plen);\n> -\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n> -\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n> -\tparse_gpg_output(sigc);\n> +\tfmt = get_format_by_sig(signature);\n> +\tif (!fmt)\n> +\t\tBUG(\"bad signature '%s'\", signature);\n\nSo if we run this from receive-pack or whatever we'll BUG() out? I.e. I\nthink this should be an fsck check or something, but not a BUG(), or\ndoes this not rely on potentially bad object-store state?\n\n> +static char *get_ssh_key_fingerprint(const char *signing_key) {\n> +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n> +\tstruct strbuf **fingerprint;\n> +\n> +\t/* For SSH Signing this can contain a filename or a public key\n> +\t* For textual representation we usually want a fingerprint\n> +\t*/\n> +\tif (istarts_with(signing_key, \"ssh-\")) {\n> +\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n> +\t\t\t\t\t\"-lf\", \"-\",\n> +\t\t\t\t\tNULL);\n> +\t\tret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0,  NULL, 0);\n> +\t} else {\n> +\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n> +\t\t\t\t\t\"-lf\", configured_signing_key,\n> +\t\t\t\t\tNULL);\n> +\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);\n> +\t\tif (!!ret)\n> +\t\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n> +\t\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n> +\t\tif (fingerprint[1]) {\n> +\t\t\treturn strbuf_detach(fingerprint[1], NULL);\n> +\t\t}\n> +\t}\n> +\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n> +}\n\nHer you declare a ret that's not used at all in the \"istarts_with\"\nbranch, and we fall through to die_errno()?\n\n[I stopped reading mostly at this point]\n\n> [...]\n> +# test_expect_success GPGSSH 'detect fudged commit with double signature' '\n> +# \tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n> +# \tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n> +# \t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n> +# \tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n> +# \tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n> +# \tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n> +# \t\tdouble-combined.asc > double-gpgsig &&\n> +# \tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n> +# \tgit hash-object -w -t commit double-commit >double-commit.commit &&\n> +# \ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n> +# \tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n> +# \tgrep \"BAD signature from\" double-actual &&\n> +# \tgrep \"Good signature from\" double-actual\n> +# '\n> +\n> +# test_expect_success GPGSSH 'show double signature with custom format' '\n> +# \tcat >expect <<-\\EOF &&\n> +# \tE\n> +\n> +\n> +\n> +\n> +# \tEOF\n> +# \tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n> +# \ttest_cmp expect actual\n> +# '\n\nPerhaps you're looking for test_expect_failure for TODO tests?\n\nI think this patch is *way* past the point of benefitting from being\nsplit into a patch series. It grew from ~200 lines added to ~1k.\n"},{"id":"429829","messageId":"ddba5667-346d-de64-ac6a-a27d78bc266d@gigacodes.de","threadId":"56054","inReplyTo":"87y2ab30yr.fsf@evledraar.gmail.com","subject":"Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-12T20:35:55Z","receivedAt":"2021-07-12T20:36:01Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 12.07.21 18:55, Ævar Arnfjörð Bjarmason wrote:\n\nI'll change all the whitespace / comments / style issues with the next \ncommit. Thanks\n>> +\t\tsigc->result = 'G';\n>> +\t\tsigc->trust_level = TRUST_FULLY;\n>> +\n>> +\t\tnext = strchrnul(output, ' '); // 'principal'\n>> +\t\treplace_cstring(&sigc->signer, output, next);\n>> +\t\toutput = next + 1;\n>> +\t\tnext = strchrnul(output, ' '); // 'with'\n>> +\t\toutput = next + 1;\n>> +\t\tnext = strchrnul(output, ' '); // KEY Type\n>> +\t\toutput = next + 1;\n>> +\t\tnext = strchrnul(output, ' '); // 'key'\n>> +\t\toutput = next + 1;\n> FWIW for new code we'd probably use string_list_split() or\n> string_list_split_in_place() or strbuf_split_buf() or something, but I\n> see this is following the existing pattern in the file...\nI agree. This is my first patch in the git codebase so it takes a bit \ngetting used to all the available utilities.\n>> +\tconst char *payload, size_t payload_size,\n>> +\tconst char *signature, size_t signature_size)\n>>   {\n>> -\tstruct child_process gpg = CHILD_PROCESS_INIT;\n>> -\tstruct gpg_format *fmt;\n>> +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n>>   \tstruct tempfile *temp;\n>>   \tint ret;\n>> -\tstruct strbuf buf = STRBUF_INIT;\n>> +\tconst char *line;\n>> +\tsize_t trust_size;\n>> +\tchar *principal;\n>> +\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n>> +\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n>>   \n>>   \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n>>   \tif (!temp)\n>> @@ -279,29 +342,125 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>>   \t\treturn -1;\n>>   \t}\n>>   \n>> -\tfmt = get_format_by_sig(signature);\n>> -\tif (!fmt)\n>> -\t\tBUG(\"bad signature '%s'\", signature);\n>> +\t// Find the principal from the  signers\n>> +\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n>> +\t\t\t\t\t\"-Y\", \"find-principals\",\n>> +\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n>> +\t\t\t\t\t\"-s\", temp->filename.buf,\n>> +\t\t\t\t\tNULL);\n>> +\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n>> +\tif (strstr(ssh_keygen_err.buf, \"unknown option\")) {\n>> +\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n>> +\t}\n>> +\tif (ret || !ssh_keygen_out.len) {\n>> +\t\t// We did not find a matching principal in the keyring - Check without validation\n>> +\t\tchild_process_init(&ssh_keygen);\n>> +\t\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n>> +\t\t\t\t\t\t\"-Y\", \"check-novalidate\",\n>> +\t\t\t\t\t\t\"-n\", \"git\",\n>> +\t\t\t\t\t\t\"-s\", temp->filename.buf,\n>> +\t\t\t\t\t\tNULL);\n>> +\t\tret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n>> +\t} else {\n>> +\t\t// Check every principal we found (one per line)\n>> +\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n> Hrm, can't we use strbuf_getline() here with the underlying io_pump API\n> that pipe_command() uses, instead of slurping it all up, and then\n> splitting on '\\n' ourselves? (I'm not sure)\nSounds good. I'll give it a try.\n>> +\t\t\twhile (*line == '\\n')\n>> +\t\t\t\tline++;\n>> +\t\t\tif (!*line)\n>> +\t\t\t\tbreak;\n>> +\n>> +\t\t\ttrust_size = strcspn(line, \" \\n\");\n>> +\t\t\tprincipal = xmemdupz(line, trust_size);\n>> +\n>> +\t\t\tchild_process_init(&ssh_keygen);\n>> +\t\t\tstrbuf_release(&ssh_keygen_out);\n>> +\t\t\tstrbuf_release(&ssh_keygen_err);\n>> +\t\t\tstrvec_push(&ssh_keygen.args,fmt->program);\n>> +\t\t\t// We found principals - Try with each until we find a match\n>> +\t\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"verify\",\n>> +\t\t\t\t\t\t\t//TODO: sprintf(\"-Overify-time=%s\", commit->date...),\n>> +\t\t\t\t\t\t\t\"-n\", \"git\",\n>> +\t\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n>> +\t\t\t\t\t\t\t\"-I\", principal,\n>> +\t\t\t\t\t\t\t\"-s\", temp->filename.buf,\n>> +\t\t\t\t\t\t\tNULL);\n>> +\n>> +\t\t\tif (ssh_revocation_file && file_exists(ssh_revocation_file)) {\n>> +\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\", ssh_revocation_file, NULL);\n> Do we want to silently ignore missing but configured revocation files?\nI'll add a warning\n>\n>> +\t\t\t}\n>> +\n>> +\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n>> +\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n>> +\t\t\t\t\t&ssh_keygen_out, 0, &ssh_keygen_err, 0);\n>> +\t\t\tsigchain_pop(SIGPIPE);\n>> +\n>> +\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n>> +\t\t\tif (ret == 0)\n>> +\t\t\t\tbreak;\n>> +\t\t}\n>> +\t}\n>> +\n>> +\tsigc->payload = xmemdupz(payload, payload_size);\n>> +\tstrbuf_stripspace(&ssh_keygen_out, 0);\n>> +\tstrbuf_stripspace(&ssh_keygen_err, 0);\n>> +\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n>> +\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n>> +\n>> +\t//sigc->gpg_output = strbuf_detach(&ssh_keygen_err, NULL); // This flip around is broken...\n> Broken how? And why the commented-out code as part of the patch?\nSorry, i should have removed it. The original code assigned gpg's stdout \nto gpg_status and stdout to gpg_output which can be a bit confusing.\n>\n>> -\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n>> -\t\t\t\t      &gpg_output, &gpg_status);\n>> -\tif (status && !gpg_output.len)\n>> -\t\tgoto out;\n>> -\tsigc->payload = xmemdupz(payload, plen);\n>> -\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n>> -\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n>> -\tparse_gpg_output(sigc);\n>> +\tfmt = get_format_by_sig(signature);\n>> +\tif (!fmt)\n>> +\t\tBUG(\"bad signature '%s'\", signature);\n> So if we run this from receive-pack or whatever we'll BUG() out? I.e. I\n> think this should be an fsck check or something, but not a BUG(), or\n> does this not rely on potentially bad object-store state?\nThe BUG() call is also from the original code. I agree that it should be \nhandled differently.\nUnfortunately this call is also the reason that when trying to verify a \nnew SSH signature with a current git version you'll get a segfault from \nthis BUG() :/\nI'm not sure if i can do anything about this other than adding a \ncompletely new tag in the commit itself instead of \"gpgsig\" which might \nbe quite involved. I haven't looked into that too much yet.\n>\n>> +static char *get_ssh_key_fingerprint(const char *signing_key) {\n>> +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n>> +\tint ret = -1;\n>> +\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n>> +\tstruct strbuf **fingerprint;\n>> +\n>> +\t/* For SSH Signing this can contain a filename or a public key\n>> +\t* For textual representation we usually want a fingerprint\n>> +\t*/\n>> +\tif (istarts_with(signing_key, \"ssh-\")) {\n>> +\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n>> +\t\t\t\t\t\"-lf\", \"-\",\n>> +\t\t\t\t\tNULL);\n>> +\t\tret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0,  NULL, 0);\n>> +\t} else {\n>> +\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n>> +\t\t\t\t\t\"-lf\", configured_signing_key,\n>> +\t\t\t\t\tNULL);\n>> +\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);\n>> +\t\tif (!!ret)\n>> +\t\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n>> +\t\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n>> +\t\tif (fingerprint[1]) {\n>> +\t\t\treturn strbuf_detach(fingerprint[1], NULL);\n>> +\t\t}\n>> +\t}\n>> +\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n>> +}\n> Her you declare a ret that's not used at all in the \"istarts_with\"\n> branch, and we fall through to die_errno()?\nI'll clean up the logic. Thanks\n>\n> [I stopped reading mostly at this point]\n>\n>> [...]\n>> +# test_expect_success GPGSSH 'detect fudged commit with double signature' '\n>> +# \tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n>> +# \tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n>> +# \t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n>> +# \tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n>> +# \tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n>> +# \tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n>> +# \t\tdouble-combined.asc > double-gpgsig &&\n>> +# \tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n>> +# \tgit hash-object -w -t commit double-commit >double-commit.commit &&\n>> +# \ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n>> +# \tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n>> +# \tgrep \"BAD signature from\" double-actual &&\n>> +# \tgrep \"Good signature from\" double-actual\n>> +# '\n>> +\n>> +# test_expect_success GPGSSH 'show double signature with custom format' '\n>> +# \tcat >expect <<-\\EOF &&\n>> +# \tE\n>> +\n>> +\n>> +\n>> +\n>> +# \tEOF\n>> +# \tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n>> +# \ttest_cmp expect actual\n>> +# '\n> Perhaps you're looking for test_expect_failure for TODO tests?\nYes. Although this test explicitly i'm having a hard time to duplicate \nfor ssh. I'm still trying to find out if the duplicate signature thing \nis actually an issue with ssh.\n>\n> I think this patch is *way* past the point of benefitting from being\n> split into a patch series. It grew from ~200 lines added to ~1k.\nSure, I can easily split the patch into seperate commits. But do i \ncreate a v3 patch from this or issue a new pull request?\nThe diff between v2 & v3 would be quite useless otherwise wouldn't it?\n\nAnd maybe another beginner contribution question:\n\nWhen i make changes to a patchset do i put new changes from the review \non top as new commits or do i edit the existing commits?\nIf so what is the workflow you normally use for this? fixup commits? I \nknow about those but haven't worked with them before.\n\nThanks for your help!\n\n"},{"id":"429838","messageId":"60ecb144df896_a68ed20864@natae.notmuch","threadId":"56054","inReplyTo":"ddba5667-346d-de64-ac6a-a27d78bc266d@gigacodes.de","subject":"Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-07-12T21:16:52Z","receivedAt":"2021-07-12T21:16:56Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Fabian Stelzer wrote:\n> On 12.07.21 18:55, Ævar Arnfjörð Bjarmason wrote:\n\n> > I think this patch is *way* past the point of benefitting from being\n> > split into a patch series. It grew from ~200 lines added to ~1k.\n> Sure, I can easily split the patch into seperate commits. But do i \n> create a v3 patch from this or issue a new pull request?\n> The diff between v2 & v3 would be quite useless otherwise wouldn't it?\n\nThe interdiff might be quite useless, but not the rangediff. Either way\nboth of those are merely tools to visualize changes between versions,\nultimately what really matters is the final commits themselves.\n\nMoreover, not all reviewers have seen every version, so for example if\nyou properly split this patch, I might join the review process at v3,\nand I don't really care what was in v2, therefore I wouldn't look at the\nrangediff.\n\n> And maybe another beginner contribution question:\n> \n> When i make changes to a patchset do i put new changes from the review \n> on top as new commits or do i edit the existing commits?\n\nEdit existing commits.\n\n> If so what is the workflow you normally use for this? fixup commits? I \n> know about those but haven't worked with them before.\n\n`git rebase --interactive` is what I use, and I think that's what most\npeople use.\n\nThis allows you to easily edit commits and add specific changes to\nspecific commits.\n\nOnce you are familiar with this process it's easier to understand fixup\ncommits, but I'd say rebasing comes first.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"430025","messageId":"2f8452f6570b1811682863441020a6e43fc556c7.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 2/9] ssh signing: add documentation","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:06Z","receivedAt":"2021-07-14T12:10:18Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  | 35 +++++++++++++++++++++++++++++++++--\n Documentation/config/user.txt |  6 ++++++\n 2 files changed, 39 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..16af0b0ada8 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\n@@ -33,3 +33,34 @@ gpg.minTrustLevel::\n * `marginal`\n * `fully`\n * `ultimate`\n+\n+gpg.ssh.keyring::\n+\tA file containing all valid SSH public signing keys.\n+\tSimilar to an .ssh/authorized_keys file.\n+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n+\tIf a signing key is found in this file then the trust level will\n+\tbe set to \"fully\". Otherwise if the key is not present\n+\tbut the signature is still valid then the trust level will be \"undefined\".\n+\n+\tThis file can be set to a location outside of the repository\n+\tand every developer maintains their own trust store.\n+\tA central repository server could generate this file automatically\n+\tfrom ssh keys with push\taccess to verify the code against.\n+\tIn a corporate setting this file is probably generated at a global location\n+\tfrom some automation that already handles developer ssh keys.\n+\n+\tA repository that is only allowing signed commits can store the file\n+\tin the repository itself using a relative path. This way only committers\n+\twith an already valid key can add or change keys in the keyring.\n+\n+\tUsing a SSH CA key with the cert-authority option\n+\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n+\n+\tTo revoke a key place the public key without the principal into the\n+\trevocationKeyring.\n+\n+gpg.ssh.revocationKeyring::\n+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n+\tSee ssh-keygen(1) for details.\n+\tIf a public key is found in this file then it will always be treated\n+\tas having trust level \"never\" and signatures will show as invalid.\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..b3c2f2c541e 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,9 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n+\tcorresponds to the private key used for signing. The private key\n+\tneeds to be available via ssh-agent. Alternatively it can be set to\n+\ta file containing a private key directly. If not set git will call\n+\t\"ssh-add -L\" and try to use the first key available.\n-- \ngitgitgadget\n\n"},{"id":"430026","messageId":"390a8f816cda0574cabe49e9f88ae1803142fb51.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 1/9] Add commit, tag & push signing via SSH keys","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:05Z","receivedAt":"2021-07-14T12:10:20Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nOpenssh v8.2p1 added some new options to ssh-keygen for signature\ncreation and verification. These allow us to use ssh keys for git\nsignatures easily.\n\nStart with adding the new signature format, new config options and\nrename some fields for consistency.\n\nThis feature makes git signing much more accessible to the average user.\nUsually they have a SSH Key for pushing code already. Using it\nfor signing commits allows us to verify not only the transport but the\npushed code as well.\n\nIn our corporate environemnt we use PIV x509 Certs on Yubikeys for email\nsigning/encryption and ssh keys which i think is quite common\n(at least for the email part). This way we can establish the correct\ntrust for the SSH Keys without setting up a separate GPG Infrastructure\n(which is still quite painful for users) or implementing x509 signing\nsupport for git (which lacks good forwarding mechanisms).\nUsing ssh agent forwarding makes this feature easily usable in todays\ndevelopment environments where code is often checked out in remote VMs / containers.\nIn such a setup the keyring & revocationKeyring can be centrally\ngenerated from the x509 CA information and distributed to the users.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n fmt-merge-msg.c |   4 +-\n gpg-interface.c | 122 +++++++++++++++++++++++++++++++++---------------\n gpg-interface.h |   5 +-\n log-tree.c      |   8 ++--\n pretty.c        |   4 +-\n 5 files changed, 95 insertions(+), 48 deletions(-)\n\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 0f66818e0f8..1d7b64fa021 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tlen = payload.len;\n \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n \t\t\t\t\t sig.len, &sigc) &&\n-\t\t\t\t!sigc.gpg_output)\n+\t\t\t\t!sigc.output)\n \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n \t\t\telse\n-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n \t\t}\n \t\tsignature_check_clear(&sigc);\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..3c9a48c8e7e 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -8,6 +8,7 @@\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+const char *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -35,6 +36,14 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = {\n+\tNULL\n+};\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static struct gpg_format gpg_format[] = {\n \t{ .name = \"openpgp\", .program = \"gpg\",\n \t  .verify_args = openpgp_verify_args,\n@@ -44,6 +53,9 @@ static struct gpg_format gpg_format[] = {\n \t  .verify_args = x509_verify_args,\n \t  .sigs = x509_sigs\n \t},\n+\t{ .name = \"ssh\", .program = \"ssh-keygen\",\n+\t  .verify_args = ssh_verify_args,\n+\t  .sigs = ssh_sigs },\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -72,7 +84,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\n-\tFREE_AND_NULL(sigc->gpg_output);\n+\tFREE_AND_NULL(sigc->output);\n \tFREE_AND_NULL(sigc->gpg_status);\n \tFREE_AND_NULL(sigc->signer);\n \tFREE_AND_NULL(sigc->key);\n@@ -257,16 +269,15 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n-static int verify_signed_buffer(const char *payload, size_t payload_size,\n-\t\t\t\tconst char *signature, size_t signature_size,\n-\t\t\t\tstruct strbuf *gpg_output,\n-\t\t\t\tstruct strbuf *gpg_status)\n+static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,\n+\tconst char *payload, size_t payload_size,\n+\tconst char *signature, size_t signature_size)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n-\tstruct gpg_format *fmt;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf gpg_out = STRBUF_INIT;\n+\tstruct strbuf gpg_err = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -279,29 +290,28 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\treturn -1;\n \t}\n \n-\tfmt = get_format_by_sig(signature);\n-\tif (!fmt)\n-\t\tBUG(\"bad signature '%s'\", signature);\n-\n \tstrvec_push(&gpg.args, fmt->program);\n \tstrvec_pushv(&gpg.args, fmt->verify_args);\n \tstrvec_pushl(&gpg.args,\n-\t\t     \"--status-fd=1\",\n-\t\t     \"--verify\", temp->filename.buf, \"-\",\n-\t\t     NULL);\n-\n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n+\t\t\t\"--status-fd=1\",\n+\t\t\t\"--verify\", temp->filename.buf, \"-\",\n+\t\t\tNULL);\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n+\tret = pipe_command(&gpg, payload, payload_size, &gpg_out, 0,\n+\t\t\t\t&gpg_err, 0);\n \tsigchain_pop(SIGPIPE);\n+\tret |= !strstr(gpg_out.buf, \"\\n[GNUPG:] GOODSIG \");\n \n-\tdelete_tempfile(&temp);\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tsigc->output = strbuf_detach(&gpg_err, NULL);\n+\tsigc->gpg_status = strbuf_detach(&gpg_out, NULL);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n-\tstrbuf_release(&buf); /* no matter it was used or not */\n+\tparse_gpg_output(sigc);\n+\n+\tdelete_tempfile(&temp);\n+\tstrbuf_release(&gpg_out);\n+\tstrbuf_release(&gpg_err);\n \n \treturn ret;\n }\n@@ -309,35 +319,36 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n-\tstruct strbuf gpg_output = STRBUF_INIT;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct gpg_format *fmt;\n \tint status;\n \n \tsigc->result = 'N';\n \tsigc->trust_level = -1;\n \n-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n-\t\t\t\t      &gpg_output, &gpg_status);\n-\tif (status && !gpg_output.len)\n-\t\tgoto out;\n-\tsigc->payload = xmemdupz(payload, plen);\n-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tfmt = get_format_by_sig(signature);\n+\tif (!fmt) {\n+\t\terror(_(\"bad/incompatible signature '%s'\"), signature);\n+\t\treturn -1;\n+\t}\n+\n+\tif (!strcmp(fmt->name, \"ssh\")) {\n+\t\tstatus = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);\n+\t} else {\n+\t\tstatus = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);\n+\t}\n+\tif (status && !sigc->output)\n+\t\treturn !!status;\n+\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n- out:\n-\tstrbuf_release(&gpg_status);\n-\tstrbuf_release(&gpg_output);\n-\n \treturn !!status;\n }\n \n void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n {\n \tconst char *output = flags & GPG_VERIFY_RAW ?\n-\t\tsigc->gpg_status : sigc->gpg_output;\n+\t\tsigc->gpg_status : sigc->output;\n \n \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n \t\tfputs(sigc->payload, stdout);\n@@ -388,12 +399,32 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tint ret;\n \n \tif (!strcmp(var, \"user.signingkey\")) {\n+\t\t/*\n+\t\t * user.signingkey can contain one of the following\n+\t\t * when format = openpgp/x509\n+\t\t *   - GPG KeyID\n+\t\t * when format = ssh\n+\t\t *   - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)\n+\t\t *   - path to a file containing a public or a private ssh key\n+\t\t */\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n \t\tset_signing_key(value);\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.keyring\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationkeyring\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.format\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\n@@ -425,6 +456,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -437,7 +471,19 @@ const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_default_ssh_signing_key();\n+\t} else {\n+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n+\t}\n+}\n+\n+const char *get_ssh_allowed_signers(void)\n+{\n+\tif (ssh_allowed_signers)\n+\t\treturn ssh_allowed_signers;\n+\n+\tdie(\"A Path to an allowed signers ssh keyring is needed for validation\");\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..5dfd92b81f6 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -17,8 +17,8 @@ enum signature_trust_level {\n \n struct signature_check {\n \tchar *payload;\n-\tchar *gpg_output;\n-\tchar *gpg_status;\n+\tchar *output;\n+\tchar *gpg_status; /* Only used internally -> remove from this public api */\n \n \t/*\n \t * possible \"result\":\n@@ -64,6 +64,7 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+const char *get_ssh_allowed_signers(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\ndiff --git a/log-tree.c b/log-tree.c\nindex 7b823786c2c..20af9bd1c82 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit)\n \n \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n \t\t\t\t signature.len, &sigc);\n-\tif (status && !sigc.gpg_output)\n+\tif (status && !sigc.output)\n \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n \telse\n-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n+\t\tshow_sig_lines(opt, status, sigc.output);\n \tsignature_check_clear(&sigc);\n \n  out:\n@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,\n \t\t/* could have a good signature */\n \t\tstatus = check_signature(payload.buf, payload.len,\n \t\t\t\t\t signature.buf, signature.len, &sigc);\n-\t\tif (sigc.gpg_output)\n-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n+\t\tif (sigc.output)\n+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n \t\telse\n \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n \t\tsignature_check_clear(&sigc);\ndiff --git a/pretty.c b/pretty.c\nindex b1ecd039cef..daa71394efd 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n \t\tswitch (placeholder[1]) {\n \t\tcase 'G':\n-\t\t\tif (c->signature_check.gpg_output)\n-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n+\t\t\tif (c->signature_check.output)\n+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n \t\t\tbreak;\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n-- \ngitgitgadget\n\n"},{"id":"430028","messageId":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v2.git.git.1626092359713.gitgitgadget@gmail.com","subject":"[PATCH v3 0/9] RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:04Z","receivedAt":"2021-07-14T12:10:21Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"I have added support for using keyfiles directly, lots of tests and\ngenerally cleaned up the signing & verification code a lot.\n\nI can still rename things from being gpg specific to a more general\n\"signing\" but thats rather cosmetic. Also i'm not sure if i named the new\ntest files correctly.\n\nThere is a patch in the pipeline for openssh by Damien Miller that will add\nvalid-after, valid-before options to the allowed keys keyring. This allows\nus to pass the commit timestamp to the verification call and make key\nrollover possible and still be able to verify older commits. Set\nvalid-after=NOW when adding your key to the keyring and set valid-before to\nmake it fail if used after a certain date. Software like gitolite/github or\ncorporate automation can do this automatically when ssh push keys are addded\n/ removed\n\nv3 addresses some issues & refactoring and splits the large commit into\nseveral smaller ones.\n\nFabian Stelzer (9):\n  Add commit, tag & push signing via SSH keys\n  ssh signing: add documentation\n  ssh signing: retrieve a default key from ssh-agent\n  ssh signing: sign using either gpg or ssh keys\n  ssh signing: provide a textual representation of the signing key\n  ssh signing: parse ssh-keygen output and verify signatures\n  ssh signing: add test prereqs\n  ssh signing: duplicate t7510 tests for commits\n  ssh signing: add more tests for logs, tags & push certs\n\n Documentation/config/gpg.txt     |  35 ++-\n Documentation/config/user.txt    |   6 +\n builtin/receive-pack.c           |   2 +\n fmt-merge-msg.c                  |   4 +-\n gpg-interface.c                  | 411 +++++++++++++++++++++++++++----\n gpg-interface.h                  |  12 +-\n log-tree.c                       |   8 +-\n pretty.c                         |   4 +-\n send-pack.c                      |   8 +-\n t/lib-gpg.sh                     |  27 ++\n t/t4202-log.sh                   |  23 ++\n t/t5534-push-signed.sh           | 101 ++++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++++\n t/t7527-signed-commit-ssh.sh     | 398 ++++++++++++++++++++++++++++++\n 14 files changed, 1136 insertions(+), 64 deletions(-)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n create mode 100755 t/t7527-signed-commit-ssh.sh\n\n\nbase-commit: d486ca60a51c9cb1fe068803c3f540724e95e83a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v3\nPull-Request: https://github.com/git/git/pull/1041\n\nRange-diff vs v2:\n\n  -:  ----------- >  1:  390a8f816cd Add commit, tag & push signing via SSH keys\n  -:  ----------- >  2:  2f8452f6570 ssh signing: add documentation\n  -:  ----------- >  3:  b84b2812470 ssh signing: retrieve a default key from ssh-agent\n  -:  ----------- >  4:  df55b9e1d59 ssh signing: sign using either gpg or ssh keys\n  -:  ----------- >  5:  0581c72634c ssh signing: provide a textual representation of the signing key\n  -:  ----------- >  6:  381a950a6e1 ssh signing: parse ssh-keygen output and verify signatures\n  -:  ----------- >  7:  1d292a8d7a2 ssh signing: add test prereqs\n  1:  b8b16f8e6ec !  8:  338d1b976e9 Add commit, tag & push signing/verification via SSH keys using ssh-keygen\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    Add commit, tag & push signing/verification via SSH keys using ssh-keygen\n     -\n     -    Openssh v8.2p1 added some new options to ssh-keygen for signature\n     -    creation and verification. These allow us to use ssh keys for git\n     -    signatures easily.\n     -\n     -    Set gpg.format = ssh and user.signingkey to either a ssh public key\n     -    string (like from an authorized_keys file), or a ssh key file.\n     -    If the key file or the config value itself contains only a public key\n     -    then the private key needs to be available via ssh-agent.\n     -    If no signingkey is set then git will call 'ssh-add -L' to check for\n     -    available agent keys and use the first one for signing.\n     -\n     -    Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) \"ALLOWED\n     -    SIGNERS\") which contains valid public keys and an principal (usually\n     -    user@domain). Depending on the environment this file can be managed by\n     -    the individual developer or for example generated by the central\n     -    repository server from known ssh keys with push access. If the\n     -    repository only allows signed commits / pushes then the file can even be\n     -    stored inside it.\n     -\n     -    To revoke a key put the public key without the principal prefix into\n     -    gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n     -    \"KEY REVOCATION LISTS\"). The same considerations about who to trust for\n     -    verification as with the keyring file apply.\n     -\n     -    This feature makes git signing much more accessible to the average user.\n     -    Usually they have a SSH Key for pushing code already. Using it\n     -    for signing commits allows us to verify not only the transport but the\n     -    pushed code as well.\n     -\n     -    In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n     -    signing/encryption and ssh keys which i think is quite common\n     -    (at least for the email part). This way we can establish the correct\n     -    trust for the SSH Keys without setting up a separate GPG Infrastructure\n     -    (which is still quite painful for users) or implementing x509 signing\n     -    support for git (which lacks good forwarding mechanisms).\n     -    Using ssh agent forwarding makes this feature easily usable in todays\n     -    development environments where code is often checked out in remote VMs / containers.\n     -    In such a setup the keyring & revocationKeyring can be centrally\n     -    generated from the x509 CA information and distributed to the users.\n     +    ssh signing: duplicate t7510 tests for commits\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     - ## Documentation/config/gpg.txt ##\n     -@@ Documentation/config/gpg.txt: gpg.program::\n     - \n     - gpg.format::\n     - \tSpecifies which key format to use when signing with `--gpg-sign`.\n     --\tDefault is \"openpgp\" and another possible value is \"x509\".\n     -+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n     - \n     - gpg.<format>.program::\n     - \tUse this to customize the program used for the signing format you\n     - \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n     - \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n     --\tvalue for `gpg.x509.program` is \"gpgsm\".\n     -+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n     - \n     - gpg.minTrustLevel::\n     - \tSpecifies a minimum trust level for signature verification.  If\n     -@@ Documentation/config/gpg.txt: gpg.minTrustLevel::\n     - * `marginal`\n     - * `fully`\n     - * `ultimate`\n     -+\n     -+gpg.ssh.keyring::\n     -+\tA file containing all valid SSH public signing keys. \n     -+\tSimilar to an .ssh/authorized_keys file.\n     -+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n     -+\tIf a signing key is found in this file then the trust level will\n     -+\tbe set to \"fully\". Otherwise if the key is not present\n     -+\tbut the signature is still valid then the trust level will be \"undefined\".\n     -+\n     -+\tThis file can be set to a location outside of the repository\n     -+\tand every developer maintains their own trust store.\n     -+\tA central repository server could generate this file automatically\n     -+\tfrom ssh keys with push\taccess to verify the code against.\n     -+\tIn a corporate setting this file is probably generated at a global location\n     -+\tfrom some automation that already handles developer ssh keys. \n     -+\t\n     -+\tA repository that is only allowing signed commits can store the file \n     -+\tin the repository itself using a relative path. This way only committers\n     -+\twith an already valid key can add or change keys in the keyring.\n     -+\n     -+\tUsing a SSH CA key with the cert-authority option \n     -+\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n     -+\n     -+\tTo revoke a key place the public key without the principal into the \n     -+\trevocationKeyring.\n     -+\n     -+gpg.ssh.revocationKeyring::\n     -+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n     -+\tSee ssh-keygen(1) for details.\n     -+\tIf a public key is found in this file then it will always be treated\n     -+\tas having trust level \"never\" and signatures will show as invalid.\n     -\n     - ## Documentation/config/user.txt ##\n     -@@ Documentation/config/user.txt: user.signingKey::\n     - \tcommit, you can override the default selection with this variable.\n     - \tThis option is passed unchanged to gpg's --local-user parameter,\n     - \tso you may specify a key using any method that gpg supports.\n     -+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n     -+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and \n     -+\tcorresponds to the private key used for signing. The private key \n     -+\tneeds to be available via ssh-agent. Alternatively it can be set to\n     -+\ta file containing a private key directly. If not set git will call \n     -+\t\"ssh-add -L\" and try to use the first key available.\n     -\n     - ## builtin/receive-pack.c ##\n     -@@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const char *value, void *cb)\n     - {\n     - \tint status = parse_hide_refs_config(var, value, \"receive\");\n     - \n     -+\tgit_gpg_config(var, value, NULL);\n     -+\n     - \tif (status)\n     - \t\treturn status;\n     - \n     -@@ builtin/receive-pack.c: static void prepare_push_cert_sha1(struct child_process *proc)\n     - \t\tbogs = parse_signed_buffer(push_cert.buf, push_cert.len);\n     - \t\tcheck_signature(push_cert.buf, bogs, push_cert.buf + bogs,\n     - \t\t\t\tpush_cert.len - bogs, &sigcheck);\n     --\n     -+\t\t\n     - \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n     - \t}\n     - \tif (!is_null_oid(&push_cert_oid)) {\n     -\n     - ## fmt-merge-msg.c ##\n     -@@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)\n     - \t\t\tlen = payload.len;\n     - \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n     - \t\t\t\t\t sig.len, &sigc) &&\n     --\t\t\t\t!sigc.gpg_output)\n     -+\t\t\t\t!sigc.output)\n     - \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n     - \t\t\telse\n     --\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n     -+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n     - \t\t}\n     - \t\tsignature_check_clear(&sigc);\n     - \n     -\n     - ## gpg-interface.c ##\n     -@@\n     - #include \"config.h\"\n     - #include \"run-command.h\"\n     - #include \"strbuf.h\"\n     -+#include \"dir.h\"\n     - #include \"gpg-interface.h\"\n     - #include \"sigchain.h\"\n     - #include \"tempfile.h\"\n     - \n     - static char *configured_signing_key;\n     -+const char *ssh_allowed_signers, *ssh_revocation_file;\n     - static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n     - \n     - struct gpg_format {\n     -@@ gpg-interface.c: static const char *x509_sigs[] = {\n     - \tNULL\n     - };\n     - \n     -+static const char *ssh_verify_args[] = {\n     -+\tNULL\n     -+};\n     -+static const char *ssh_sigs[] = {\n     -+\t\"-----BEGIN SSH SIGNATURE-----\",\n     -+\tNULL\n     -+};\n     -+\n     - static struct gpg_format gpg_format[] = {\n     - \t{ .name = \"openpgp\", .program = \"gpg\",\n     - \t  .verify_args = openpgp_verify_args,\n     -@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     - \t  .verify_args = x509_verify_args,\n     - \t  .sigs = x509_sigs\n     - \t},\n     -+\t{ .name = \"ssh\", .program = \"ssh-keygen\",\n     -+\t  .verify_args = ssh_verify_args,\n     -+\t  .sigs = ssh_sigs },\n     - };\n     - \n     - static struct gpg_format *use_format = &gpg_format[0];\n     -@@ gpg-interface.c: static struct gpg_format *get_format_by_sig(const char *sig)\n     - void signature_check_clear(struct signature_check *sigc)\n     - {\n     - \tFREE_AND_NULL(sigc->payload);\n     -+\tFREE_AND_NULL(sigc->output);\n     - \tFREE_AND_NULL(sigc->gpg_output);\n     - \tFREE_AND_NULL(sigc->gpg_status);\n     - \tFREE_AND_NULL(sigc->signer);\n     -@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,\n     - \treturn 1;\n     - }\n     - \n     -+static void parse_ssh_output(struct signature_check *sigc)\n     -+{\n     -+\tconst char *output = NULL;\n     -+\tchar *next = NULL;\n     -+\n     -+\t/* ssh-keysign output should be:\n     -+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n     -+\t * or for valid but unknown keys:\n     -+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n     -+\t */\n     -+\n     -+\toutput = xmemdupz(sigc->output, strcspn(sigc->output, \" \\n\"));\n     -+\tif (skip_prefix(sigc->output, \"Good \\\"git\\\" signature for \", &output)) {\n     -+\t\t// Valid signature for a trusted signer\n     -+\t\tsigc->result = 'G';\n     -+\t\tsigc->trust_level = TRUST_FULLY;\n     -+\n     -+\t\tnext = strchrnul(output, ' '); // 'principal'\n     -+\t\treplace_cstring(&sigc->signer, output, next);\n     -+\t\toutput = next + 1;\n     -+\t\tnext = strchrnul(output, ' '); // 'with'\n     -+\t\toutput = next + 1;\n     -+\t\tnext = strchrnul(output, ' '); // KEY Type\n     -+\t\toutput = next + 1;\n     -+\t\tnext = strchrnul(output, ' '); // 'key'\n     -+\t\toutput = next + 1;\n     -+\t\tnext = strchrnul(output, '\\n'); // key\n     -+\t\treplace_cstring(&sigc->fingerprint, output, next);\n     -+\t\treplace_cstring(&sigc->key, output, next);\n     -+\t} else if (skip_prefix(sigc->output, \"Good \\\"git\\\" signature with \", &output)) {\n     -+\t\t// Valid signature, but key unknown\n     -+\t\tsigc->result = 'G';\n     -+\t\tsigc->trust_level = TRUST_UNDEFINED;\n     -+\n     -+\t\tnext = strchrnul(output, ' '); // KEY Type\n     -+\t\toutput = next + 1;\n     -+\t\tnext = strchrnul(output, ' '); // 'key'\n     -+\t\toutput = next + 1;\n     -+\t\tnext = strchrnul(output, '\\n'); // key\n     -+\t\treplace_cstring(&sigc->fingerprint, output, next);\n     -+\t\treplace_cstring(&sigc->key, output, next);\n     -+\t} else {\n     -+\t\tsigc->result = 'B';\n     -+\t\tsigc->trust_level = TRUST_NEVER;\n     -+\t}\n     -+}\n     -+\n     - static void parse_gpg_output(struct signature_check *sigc)\n     - {\n     - \tconst char *buf = sigc->gpg_status;\n     -@@ gpg-interface.c: error:\n     - \tFREE_AND_NULL(sigc->key);\n     - }\n     - \n     --static int verify_signed_buffer(const char *payload, size_t payload_size,\n     --\t\t\t\tconst char *signature, size_t signature_size,\n     --\t\t\t\tstruct strbuf *gpg_output,\n     --\t\t\t\tstruct strbuf *gpg_status)\n     -+static int verify_ssh_signature(struct signature_check *sigc, struct gpg_format *fmt,\n     -+\tconst char *payload, size_t payload_size,\n     -+\tconst char *signature, size_t signature_size)\n     - {\n     --\tstruct child_process gpg = CHILD_PROCESS_INIT;\n     --\tstruct gpg_format *fmt;\n     -+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n     - \tstruct tempfile *temp;\n     - \tint ret;\n     --\tstruct strbuf buf = STRBUF_INIT;\n     -+\tconst char *line;\n     -+\tsize_t trust_size;\n     -+\tchar *principal;\n     -+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n     -+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n     - \n     - \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n     - \tif (!temp)\n     -@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,\n     - \t\treturn -1;\n     - \t}\n     - \n     --\tfmt = get_format_by_sig(signature);\n     --\tif (!fmt)\n     --\t\tBUG(\"bad signature '%s'\", signature);\n     -+\t// Find the principal from the  signers\n     -+\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n     -+\t\t\t\t\t\"-Y\", \"find-principals\",\n     -+\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     -+\t\t\t\t\t\"-s\", temp->filename.buf,\n     -+\t\t\t\t\tNULL);\n     -+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     -+\tif (strstr(ssh_keygen_err.buf, \"unknown option\")) {\n     -+\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n     -+\t}\n     -+\tif (ret || !ssh_keygen_out.len) {\n     -+\t\t// We did not find a matching principal in the keyring - Check without validation\n     -+\t\tchild_process_init(&ssh_keygen);\n     -+\t\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n     -+\t\t\t\t\t\t\"-Y\", \"check-novalidate\",\n     -+\t\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\t\"-s\", temp->filename.buf,\n     -+\t\t\t\t\t\tNULL);\n     -+\t\tret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     -+\t} else {\n     -+\t\t// Check every principal we found (one per line)\n     -+\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n     -+\t\t\twhile (*line == '\\n')\n     -+\t\t\t\tline++;\n     -+\t\t\tif (!*line)\n     -+\t\t\t\tbreak;\n     -+\n     -+\t\t\ttrust_size = strcspn(line, \" \\n\");\n     -+\t\t\tprincipal = xmemdupz(line, trust_size);\n     -+\n     -+\t\t\tchild_process_init(&ssh_keygen);\n     -+\t\t\tstrbuf_release(&ssh_keygen_out);\n     -+\t\t\tstrbuf_release(&ssh_keygen_err);\n     -+\t\t\tstrvec_push(&ssh_keygen.args,fmt->program);\n     -+\t\t\t// We found principals - Try with each until we find a match\n     -+\t\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"verify\",\n     -+\t\t\t\t\t\t\t//TODO: sprintf(\"-Overify-time=%s\", commit->date...),\n     -+\t\t\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     -+\t\t\t\t\t\t\t\"-I\", principal,\n     -+\t\t\t\t\t\t\t\"-s\", temp->filename.buf,\n     -+\t\t\t\t\t\t\tNULL);\n     -+\n     -+\t\t\tif (ssh_revocation_file && file_exists(ssh_revocation_file)) {\n     -+\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\", ssh_revocation_file, NULL);\n     -+\t\t\t}\n     -+\n     -+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     -+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n     -+\t\t\t\t\t&ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     -+\t\t\tsigchain_pop(SIGPIPE);\n     -+\n     -+\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n     -+\t\t\tif (ret == 0)\n     -+\t\t\t\tbreak;\n     -+\t\t}\n     -+\t}\n     -+\n     -+\tsigc->payload = xmemdupz(payload, payload_size);\n     -+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n     -+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n     -+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n     -+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n     -+\n     -+\t//sigc->gpg_output = strbuf_detach(&ssh_keygen_err, NULL); // This flip around is broken...\n     -+\tsigc->gpg_status = strbuf_detach(&ssh_keygen_out, NULL);\n     -+\n     -+\tparse_ssh_output(sigc);\n     -+\n     -+\tdelete_tempfile(&temp);\n     -+\tstrbuf_release(&ssh_keygen_out);\n     -+\tstrbuf_release(&ssh_keygen_err);\n     -+\n     -+\treturn ret;\n     -+}\n     -+\n     -+static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt, \n     -+\tconst char *payload, size_t payload_size,\n     -+\tconst char *signature, size_t signature_size)\n     -+{\n     -+\tstruct child_process gpg = CHILD_PROCESS_INIT;\n     -+\tstruct tempfile *temp;\n     -+\tint ret;\n     -+\tstruct strbuf gpg_out = STRBUF_INIT;\n     -+\tstruct strbuf gpg_err = STRBUF_INIT;\n     -+\n     -+\ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n     -+\tif (!temp)\n     -+\t\treturn error_errno(_(\"could not create temporary file\"));\n     -+\tif (write_in_full(temp->fd, signature, signature_size) < 0 ||\n     -+\t    close_tempfile_gently(temp) < 0) {\n     -+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n     -+\t\t\t    temp->filename.buf);\n     -+\t\tdelete_tempfile(&temp);\n     -+\t\treturn -1;\n     -+\t}\n     - \n     - \tstrvec_push(&gpg.args, fmt->program);\n     - \tstrvec_pushv(&gpg.args, fmt->verify_args);\n     - \tstrvec_pushl(&gpg.args,\n     --\t\t     \"--status-fd=1\",\n     --\t\t     \"--verify\", temp->filename.buf, \"-\",\n     --\t\t     NULL);\n     --\n     --\tif (!gpg_status)\n     --\t\tgpg_status = &buf;\n     -+\t\t\t\"--status-fd=1\",\n     -+\t\t\t\"--verify\", temp->filename.buf, \"-\",\n     -+\t\t\tNULL);\n     - \n     - \tsigchain_push(SIGPIPE, SIG_IGN);\n     --\tret = pipe_command(&gpg, payload, payload_size,\n     --\t\t\t   gpg_status, 0, gpg_output, 0);\n     -+\tret = pipe_command(&gpg, payload, payload_size, &gpg_out, 0,\n     -+\t\t\t\t&gpg_err, 0);\n     - \tsigchain_pop(SIGPIPE);\n     -+\tret |= !strstr(gpg_out.buf, \"\\n[GNUPG:] GOODSIG \");\n     - \n     --\tdelete_tempfile(&temp);\n     -+\tsigc->payload = xmemdupz(payload, payload_size);\n     -+\tsigc->output = strbuf_detach(&gpg_err, NULL);\n     -+\tsigc->gpg_status = strbuf_detach(&gpg_out, NULL);\n     - \n     --\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n     --\tstrbuf_release(&buf); /* no matter it was used or not */\n     -+\tparse_gpg_output(sigc);\n     -+\n     -+\tdelete_tempfile(&temp);\n     -+\tstrbuf_release(&gpg_out);\n     -+\tstrbuf_release(&gpg_err);\n     - \n     - \treturn ret;\n     - }\n     -@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,\n     - int check_signature(const char *payload, size_t plen, const char *signature,\n     - \tsize_t slen, struct signature_check *sigc)\n     - {\n     --\tstruct strbuf gpg_output = STRBUF_INIT;\n     --\tstruct strbuf gpg_status = STRBUF_INIT;\n     -+\tstruct gpg_format *fmt;\n     - \tint status;\n     - \n     - \tsigc->result = 'N';\n     - \tsigc->trust_level = -1;\n     - \n     --\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n     --\t\t\t\t      &gpg_output, &gpg_status);\n     --\tif (status && !gpg_output.len)\n     --\t\tgoto out;\n     --\tsigc->payload = xmemdupz(payload, plen);\n     --\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n     --\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n     --\tparse_gpg_output(sigc);\n     -+\tfmt = get_format_by_sig(signature);\n     -+\tif (!fmt)\n     -+\t\tBUG(\"bad signature '%s'\", signature);\n     -+\n     -+\tif (!strcmp(fmt->name, \"ssh\")) {\n     -+\t\tstatus = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);\n     -+\t} else {\n     -+\t\tstatus = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);\n     -+\t}\n     -+\tif (status && !sigc->gpg_output)\n     -+\t\treturn !!status;\n     -+\n     - \tstatus |= sigc->result != 'G';\n     - \tstatus |= sigc->trust_level < configured_min_trust_level;\n     - \n     -- out:\n     --\tstrbuf_release(&gpg_status);\n     --\tstrbuf_release(&gpg_output);\n     --\n     - \treturn !!status;\n     - }\n     - \n     - void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n     - {\n     - \tconst char *output = flags & GPG_VERIFY_RAW ?\n     --\t\tsigc->gpg_status : sigc->gpg_output;\n     -+\t\tsigc->gpg_status : sigc->output;\n     - \n     - \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n     - \t\tfputs(sigc->payload, stdout);\n     -@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     - \tint ret;\n     - \n     - \tif (!strcmp(var, \"user.signingkey\")) {\n     -+\t\t/* user.signingkey can contain one of the following\n     -+\t\t * when format = openpgp/x509\n     -+\t\t *   - GPG KeyID\n     -+\t\t * when format = ssh\n     -+\t\t *   - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)\n     -+\t\t *   - path to a file containing a public or a private ssh key\n     -+\t\t */\n     - \t\tif (!value)\n     - \t\t\treturn config_error_nonbool(var);\n     - \t\tset_signing_key(value);\n     - \t\treturn 0;\n     - \t}\n     - \n     -+\tif (!strcmp(var, \"gpg.ssh.keyring\")) {\n     -+\t\tif (!value)\n     -+\t\t\treturn config_error_nonbool(var);\n     -+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n     -+\t}\n     -+\n     -+\tif (!strcmp(var, \"gpg.ssh.revocationkeyring\")) {\n     -+\t\tif (!value)\n     -+\t\t\treturn config_error_nonbool(var);\n     -+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n     -+\t}\n     -+\n     - \tif (!strcmp(var, \"gpg.format\")) {\n     - \t\tif (!value)\n     - \t\t\treturn config_error_nonbool(var);\n     -@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     - \tif (!strcmp(var, \"gpg.x509.program\"))\n     - \t\tfmtname = \"x509\";\n     - \n     -+\tif (!strcmp(var, \"gpg.ssh.program\"))\n     -+\t\tfmtname = \"ssh\";\n     -+\n     - \tif (fmtname) {\n     - \t\tfmt = get_format_by_name(fmtname);\n     - \t\treturn git_config_string(&fmt->program, var, value);\n     -@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     - \treturn 0;\n     - }\n     - \n     -+static char *get_ssh_key_fingerprint(const char *signing_key) {\n     -+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n     -+\tint ret = -1;\n     -+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n     -+\tstruct strbuf **fingerprint;\n     -+\n     -+\t/* For SSH Signing this can contain a filename or a public key\n     -+\t* For textual representation we usually want a fingerprint\n     -+\t*/\n     -+\tif (istarts_with(signing_key, \"ssh-\")) {\n     -+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n     -+\t\t\t\t\t\"-lf\", \"-\",\n     -+\t\t\t\t\tNULL);\n     -+\t\tret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0,  NULL, 0);\n     -+\t} else {\n     -+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n     -+\t\t\t\t\t\"-lf\", configured_signing_key,\n     -+\t\t\t\t\tNULL);\n     -+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);\n     -+\t\tif (!!ret)\n     -+\t\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n     -+\t\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n     -+\t\tif (fingerprint[1]) {\n     -+\t\t\treturn strbuf_detach(fingerprint[1], NULL);\n     -+\t\t}\n     -+\t}\n     -+\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"), signing_key);\n     -+}\n     -+\n     -+// Returns the first public key from an ssh-agent to use for signing\n     -+static char *get_default_ssh_signing_key(void) {\n     -+\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n     -+\tint ret = -1;\n     -+\tstruct strbuf key_stdout = STRBUF_INIT;\n     -+\tstruct strbuf **keys;\n     -+\n     -+\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n     -+\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n     -+\tif (!ret) { \n     -+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n     -+\t\tif (keys[0])\n     -+\t\t\treturn strbuf_detach(keys[0], NULL);\n     -+\t}\n     -+\n     -+\treturn \"\";\n     -+}\n     -+\n     -+// Returns a textual but unique representation ot the signing key\n     -+const char *get_signing_key_id(void) {\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\treturn get_ssh_key_fingerprint(get_signing_key());\n     -+\t} else {\n     -+\t\t// GPG/GPGSM only store a key id on this variable\n     -+\t\treturn get_signing_key();\n     -+\t}\n     -+}\n     -+\n     - const char *get_signing_key(void)\n     - {\n     - \tif (configured_signing_key)\n     - \t\treturn configured_signing_key;\n     --\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\treturn get_default_ssh_signing_key();\n     -+\t} else {\n     -+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n     -+\t}\n     -+}\n     -+\n     -+const char *get_ssh_allowed_signers(void)\n     -+{\n     -+\tif (ssh_allowed_signers)\n     -+\t\treturn ssh_allowed_signers;\n     -+\n     -+\tdie(\"A Path to an allowed signers ssh keyring is needed for validation\");\n     - }\n     - \n     - int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n     -@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n     - \tint ret;\n     - \tsize_t i, j, bottom;\n     - \tstruct strbuf gpg_status = STRBUF_INIT;\n     -+\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n     -+\tchar *ssh_signing_key_file = NULL;\n     -+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n     -+\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\tif (!signing_key || signing_key[0] == '\\0')\n     -+\t\t\treturn error(_(\"user.signingkey needs to be set for ssh signing\"));\n     -+\n     -+\n     -+\t\tif (istarts_with(signing_key, \"ssh-\")) {\n     -+\t\t\t// A literal ssh key\n     -+\t\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n     -+\t\t\tif (!temp)\n     -+\t\t\t\treturn error_errno(_(\"could not create temporary file\"));\n     -+\t\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) < 0 ||\n     -+\t\t\t\tclose_tempfile_gently(temp) < 0) {\n     -+\t\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"), temp->filename.buf);\n     -+\t\t\t\tdelete_tempfile(&temp);\n     -+\t\t\t\treturn -1;\n     -+\t\t\t}\n     -+\t\t\tssh_signing_key_file= temp->filename.buf;\n     -+\t\t} else {\n     -+\t\t\t// We assume a file\n     -+\t\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n     -+\t\t}\n     - \n     --\tstrvec_pushl(&gpg.args,\n     --\t\t     use_format->program,\n     --\t\t     \"--status-fd=2\",\n     --\t\t     \"-bsau\", signing_key,\n     --\t\t     NULL);\n     -+\t\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n     -+\t\tif (!buffer_file)\n     -+\t\t\treturn error_errno(_(\"could not create temporary file\"));\n     -+\t\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n     -+\t\t\tclose_tempfile_gently(buffer_file) < 0) {\n     -+\t\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"), buffer_file->filename.buf);\n     -+\t\t\tdelete_tempfile(&buffer_file);\n     -+\t\t\treturn -1;\n     -+\t\t}\n     -+\n     -+\t\tstrvec_pushl(&gpg.args, use_format->program ,\n     -+\t\t\t\t\t\"-Y\", \"sign\",\n     -+\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\"-f\", ssh_signing_key_file,\n     -+\t\t\t\t\tbuffer_file->filename.buf,\n     -+\t\t\t\t\tNULL);\n     -+\n     -+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     -+\t\tret = pipe_command(&gpg, NULL, 0, NULL, 0, &gpg_status, 0);\n     -+\t\tsigchain_pop(SIGPIPE);\n     -+\n     -+\t\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n     -+\t\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n     -+\t\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n     -+\t\t\terror_errno(_(\"failed reading ssh signing data buffer from '%s'\"), ssh_signature_filename.buf);\n     -+\t\t}\n     -+\t\tunlink_or_warn(ssh_signature_filename.buf);\n     -+\t\tstrbuf_release(&ssh_signature_filename);\n     -+\t\tdelete_tempfile(&buffer_file);\n     -+\t} else {\n     -+\t\tstrvec_pushl(&gpg.args, use_format->program ,\n     -+\t\t\t\t\t\"--status-fd=2\",\n     -+\t\t\t\t\t\"-bsau\", signing_key,\n     -+\t\t\t\t\tNULL);\n     -+\n     -+\t\t/*\n     -+\t\t* When the username signingkey is bad, program could be terminated\n     -+\t\t* because gpg exits without reading and then write gets SIGPIPE.\n     -+\t\t*/\n     -+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     -+\t\tret = pipe_command(&gpg, buffer->buf, buffer->len, signature, 1024, &gpg_status, 0);\n     -+\t\tsigchain_pop(SIGPIPE);\n     -+\t}\n     - \n     - \tbottom = signature->len;\n     - \n     --\t/*\n     --\t * When the username signingkey is bad, program could be terminated\n     --\t * because gpg exits without reading and then write gets SIGPIPE.\n     --\t */\n     --\tsigchain_push(SIGPIPE, SIG_IGN);\n     --\tret = pipe_command(&gpg, buffer->buf, buffer->len,\n     --\t\t\t   signature, 1024, &gpg_status, 0);\n     --\tsigchain_pop(SIGPIPE);\n     -+\tif (temp)\n     -+\t\tdelete_tempfile(&temp);\n     - \n     --\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\tif (strstr(gpg_status.buf, \"unknown option\")) {\n     -+\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n     -+\t\t}\n     -+\t} else {\n     -+\t\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n     -+\t}\n     - \tstrbuf_release(&gpg_status);\n     - \tif (ret)\n     - \t\treturn error(_(\"gpg failed to sign the data\"));\n     -\n     - ## gpg-interface.h ##\n     -@@ gpg-interface.h: enum signature_trust_level {\n     - \n     - struct signature_check {\n     - \tchar *payload;\n     --\tchar *gpg_output;\n     --\tchar *gpg_status;\n     -+\tchar *output;\n     -+\tchar *gpg_output; // This will be printed in commit logs\n     -+\tchar *gpg_status; // Only used internally -> remove\n     - \n     - \t/*\n     - \t * possible \"result\":\n     -@@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n     - int git_gpg_config(const char *, const char *, void *);\n     - void set_signing_key(const char *);\n     - const char *get_signing_key(void);\n     -+\n     -+/* Returns a textual unique representation of the signing key in use\n     -+ * Either a GPG KeyID or a SSH Key Fingerprint\n     -+ */\n     -+const char *get_signing_key_id(void);\n     -+\n     -+const char *get_ssh_allowed_signers(void);\n     - int check_signature(const char *payload, size_t plen,\n     - \t\t    const char *signature, size_t slen,\n     - \t\t    struct signature_check *sigc);\n     -\n     - ## log-tree.c ##\n     -@@ log-tree.c: static void show_signature(struct rev_info *opt, struct commit *commit)\n     - \n     - \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n     - \t\t\t\t signature.len, &sigc);\n     --\tif (status && !sigc.gpg_output)\n     -+\tif (status && !sigc.output)\n     - \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n     - \telse\n     --\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n     -+\t\tshow_sig_lines(opt, status, sigc.output);\n     - \tsignature_check_clear(&sigc);\n     - \n     -  out:\n     -@@ log-tree.c: static int show_one_mergetag(struct commit *commit,\n     - \t\t/* could have a good signature */\n     - \t\tstatus = check_signature(payload.buf, payload.len,\n     - \t\t\t\t\t signature.buf, signature.len, &sigc);\n     --\t\tif (sigc.gpg_output)\n     --\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n     -+\t\tif (sigc.output)\n     -+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n     - \t\telse\n     - \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n     - \t\tsignature_check_clear(&sigc);\n     -\n     - ## pretty.c ##\n     -@@ pretty.c: static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n     - \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n     - \t\tswitch (placeholder[1]) {\n     - \t\tcase 'G':\n     --\t\t\tif (c->signature_check.gpg_output)\n     --\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n     -+\t\t\tif (c->signature_check.output)\n     -+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n     - \t\t\tbreak;\n     - \t\tcase '?':\n     - \t\t\tswitch (c->signature_check.result) {\n     -\n     - ## send-pack.c ##\n     -@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,\n     - \tconst struct ref *ref;\n     - \tstruct string_list_item *item;\n     - \tchar *signing_key = xstrdup(get_signing_key());\n     -+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n     - \tconst char *cp, *np;\n     - \tstruct strbuf cert = STRBUF_INIT;\n     - \tint update_seen = 0;\n     --\n     -+\t\n     - \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n     --\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n     -+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n     - \tdatestamp(&cert);\n     - \tstrbuf_addch(&cert, '\\n');\n     - \tif (args->url && *args->url) {\n     -@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,\n     - \n     - free_return:\n     - \tfree(signing_key);\n     -+\tfree(signing_key_id);\n     - \tstrbuf_release(&cert);\n     - \treturn update_seen;\n     - }\n     -\n     - ## t/lib-gpg.sh ##\n     -@@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '\n     - \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n     - '\n     - \n     -+test_lazy_prereq GPGSSH '\n     -+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n     -+\ttest $? != 127 || exit 1\n     -+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n     -+\ttest $? = 0 || exit 1; \n     -+\tmkdir -p \"${GNUPGHOME}\" &&\n     -+\tchmod 0700 \"${GNUPGHOME}\" &&\n     -+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n     -+\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n     -+\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n     -+\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"principal_\\\" NR \\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n     -+\tcat \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n     -+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n     -+'\n     -+\n     -+SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n     -+SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n     -+SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n     -+SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n     -+SIGNING_KEY_PASSPHRASE=\"super_secret\"\n     -+SIGNING_KEYRING=\"${GNUPGHOME}/ssh.all_valid.keyring\"\n     -+\n     -+GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n     -+GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n     -+KEY_NOT_TRUSTED=\"No principal matched\"\n     -+BAD_SIGNATURE=\"Signature verification failed\"\n     -+\n     - sanitize_pgp() {\n     - \tperl -ne '\n     - \t\t/^-----END PGP/ and $in_pgp = 0;\n     -\n     - ## t/t4202-log.sh ##\n     -@@ t/t4202-log.sh: test_expect_success GPGSM 'setup signed branch x509' '\n     - \tgit commit -S -m signed_commit\n     - '\n     - \n     -+test_expect_success GPGSSH 'setup sshkey signed branch' '\n     -+\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n     -+\tgit checkout -b signed-ssh main &&\n     -+\techo foo >foo &&\n     -+\tgit add foo &&\n     -+\tgit commit -S -m signed_commit\n     -+'\n     -+\n     - test_expect_success GPGSM 'log x509 fingerprint' '\n     - \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n     - \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n     -@@ t/t4202-log.sh: test_expect_success GPGSM 'log --graph --show-signature x509' '\n     - \tgrep \"^| gpgsm: Good signature\" actual\n     - '\n     - \n     -+test_expect_success GPGSSH 'log ssh key fingerprint' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n     -+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     - test_expect_success GPG 'log --graph --show-signature for merged tag' '\n     - \ttest_when_finished \"git reset --hard && git checkout main\" &&\n     - \tgit checkout -b plain main &&\n     -\n     - ## t/t5534-push-signed.sh ##\n     -@@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certificate' '\n     - \ttest_cmp expect dst/push-cert-status\n     - '\n     - \n     -+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n     -+\tprepare_dst &&\n     -+\tmkdir -p dst/.git/hooks &&\n     -+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+\tgit -C dst config receive.certnonceseed sekrit &&\n     -+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n     -+\t# discard the update list\n     -+\tcat >/dev/null\n     -+\t# record the push certificate\n     -+\tif test -n \"${GIT_PUSH_CERT-}\"\n     -+\tthen\n     -+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n     -+\tfi &&\n     -+\n     -+\tcat >../push-cert-status <<E_O_F\n     -+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n     -+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n     -+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n     -+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n     -+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n     -+\tE_O_F\n     -+\n     -+\tEOF\n     -+\n     -+\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     -+\tgit push --signed dst noop ff +noff &&\n     -+\n     -+\t(\n     -+\t\tcat <<-\\EOF &&\n     -+\t\tSIGNER=principal_1\n     -+\t\tKEY=FINGERPRINT\n     -+\t\tSTATUS=G\n     -+\t\tNONCE_STATUS=OK\n     -+\t\tEOF\n     -+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n     -+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n     -+\n     -+\tnoop=$(git rev-parse noop) &&\n     -+\tff=$(git rev-parse ff) &&\n     -+\tnoff=$(git rev-parse noff) &&\n     -+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n     -+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n     -+\ttest_cmp expect dst/push-cert-status\n     -+'\n     -+\n     - test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n     - \t# First, invoke receive-pack with dummy input to obtain its preamble.\n     - \tprepare_dst &&\n     -@@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n     - \ttest_cmp expect dst/push-cert-status\n     - '\n     - \n     -+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n     -+\ttest_config gpg.format ssh &&\n     -+\tprepare_dst &&\n     -+\tmkdir -p dst/.git/hooks &&\n     -+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+\tgit -C dst config receive.certnonceseed sekrit &&\n     -+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n     -+\t# discard the update list\n     -+\tcat >/dev/null\n     -+\t# record the push certificate\n     -+\tif test -n \"${GIT_PUSH_CERT-}\"\n     -+\tthen\n     -+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n     -+\tfi &&\n     -+\n     -+\tcat >../push-cert-status <<E_O_F\n     -+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n     -+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n     -+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n     -+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n     -+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n     -+\tE_O_F\n     -+\n     -+\tEOF\n     -+\n     -+\ttest_config user.email hasnokey@nowhere.com &&\n     -+\ttest_config gpg.format ssh &&\n     -+\t\n     -+\ttest_config user.signingkey \"\" &&\n     -+\t(\n     -+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n     -+\t\ttest_must_fail git push --signed dst noop ff +noff\n     -+\t) &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     -+\tgit push --signed dst noop ff +noff &&\n     -+\n     -+\t(\n     -+\t\tcat <<-\\EOF &&\n     -+\t\tSIGNER=principal_1\n     -+\t\tKEY=FINGERPRINT\n     -+\t\tSTATUS=G\n     -+\t\tNONCE_STATUS=OK\n     -+\t\tEOF\n     -+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n     -+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n     -+\n     -+\tnoop=$(git rev-parse noop) &&\n     -+\tff=$(git rev-parse ff) &&\n     -+\tnoff=$(git rev-parse noff) &&\n     -+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n     -+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n     -+\ttest_cmp expect dst/push-cert-status\n     -+'\n     -+\n     - test_expect_success GPG 'failed atomic push does not execute GPG' '\n     - \tprepare_dst &&\n     - \tgit -C dst config receive.certnonceseed sekrit &&\n     -\n     - ## t/t7031-verify-tag-signed-ssh.sh (new) ##\n     -@@\n     -+#!/bin/sh\n     -+\n     -+test_description='signed tag tests'\n     -+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n     -+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     -+\n     -+. ./test-lib.sh\n     -+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n     -+\n     -+test_expect_success GPGSSH 'create signed tags ssh' '\n     -+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n     -+\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\n     -+\techo 1 >file && git add file &&\n     -+\ttest_tick && git commit -m initial &&\n     -+\tgit tag -s -m initial initial &&\n     -+\tgit branch side &&\n     -+\n     -+\techo 2 >file && test_tick && git commit -a -m second &&\n     -+\tgit tag -s -m second second &&\n     -+\n     -+\tgit checkout side &&\n     -+\techo 3 >elif && git add elif &&\n     -+\ttest_tick && git commit -m \"third on side\" &&\n     -+\n     -+\tgit checkout main &&\n     -+\ttest_tick && git merge -S side &&\n     -+\tgit tag -s -m merge merge &&\n     -+\n     -+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n     -+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n     -+\n     -+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n     -+\tgit tag -s -m fourth fourth-signed &&\n     -+\n     -+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n     -+\tgit tag fifth-unsigned &&\n     -+\n     -+\tgit config commit.gpgsign true &&\n     -+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n     -+\tgit tag -a -m sixth sixth-unsigned &&\n     -+\n     -+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n     -+\tgit tag -m seventh -s seventh-signed &&\n     -+\n     -+\techo 8 >file && test_tick && git commit -a -m eighth &&\n     -+\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n     -+'\n     -+\n     -+test_expect_success GPGSSH 'verify and show ssh signatures' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+\ttest_config gpg.mintrustlevel UNDEFINED &&\n     -+\t(\n     -+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n     -+\t\tdo\n     -+\t\t\tgit verify-tag $tag 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     -+\t\t\techo $tag OK || exit 1\n     -+\t\tdone\n     -+\t) &&\n     -+\t(\n     -+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n     -+\t\tdo\n     -+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n     -+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     -+\t\t\techo $tag OK || exit 1\n     -+\t\tdone\n     -+\t) &&\n     -+\t(\n     -+\t\tfor tag in eighth-signed-alt\n     -+\t\tdo\n     -+\t\t\tgit verify-tag $tag 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     -+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n     -+\t\t\techo $tag OK || exit 1\n     -+\t\tdone\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success GPGSSH 'detect fudged ssh signature' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+\tgit cat-file tag seventh-signed >raw &&\n     -+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n     -+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n     -+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n     -+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n     -+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n     -+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n     -+'\n     -+\n     -+# test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n     -+# \ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+# \t(\n     -+# \t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n     -+# \t\tdo\n     -+# \t\t\tgit verify-tag --raw $tag 2>actual &&\n     -+# \t\t\tgrep \"GOODSIG\" actual &&\n     -+# \t\t\t! grep \"BADSIG\" actual &&\n     -+# \t\t\techo $tag OK || exit 1\n     -+# \t\tdone\n     -+# \t) &&\n     -+# \t(\n     -+# \t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n     -+# \t\tdo\n     -+# \t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n     -+# \t\t\t! grep \"GOODSIG\" actual &&\n     -+# \t\t\t! grep \"BADSIG\" actual &&\n     -+# \t\t\techo $tag OK || exit 1\n     -+# \t\tdone\n     -+# \t) &&\n     -+# \t(\n     -+# \t\tfor tag in eighth-signed-alt\n     -+# \t\tdo\n     -+# \t\t\tgit verify-tag --raw $tag 2>actual &&\n     -+# \t\t\tgrep \"GOODSIG\" actual &&\n     -+# \t\t\t! grep \"BADSIG\" actual &&\n     -+# \t\t\tgrep \"TRUST_UNDEFINED\" actual &&\n     -+# \t\t\techo $tag OK || exit 1\n     -+# \t\tdone\n     -+# \t)\n     -+# '\n     -+\n     -+# test_expect_success GPGSM 'verify signatures with --raw x509' '\n     -+# \tgit verify-tag --raw ninth-signed-x509 2>actual &&\n     -+# \tgrep \"GOODSIG\" actual &&\n     -+# \t! grep \"BADSIG\" actual &&\n     -+# \techo ninth-signed-x509 OK\n     -+# '\n     -+\n     -+# test_expect_success GPGSSH 'verify multiple tags' '\n     -+# \ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+# \ttags=\"fourth-signed sixth-signed seventh-signed\" &&\n     -+# \tfor i in $tags\n     -+# \tdo\n     -+# \t\tgit verify-tag -v --raw $i || return 1\n     -+# \tdone >expect.stdout 2>expect.stderr.1 &&\n     -+# \tgrep \"^.GNUPG:.\" <expect.stderr.1 >expect.stderr &&\n     -+# \tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n     -+# \tgrep \"^.GNUPG:.\" <actual.stderr.1 >actual.stderr &&\n     -+# \ttest_cmp expect.stdout actual.stdout &&\n     -+# \ttest_cmp expect.stderr actual.stderr\n     -+# '\n     -+\n     -+# test_expect_success GPGSM 'verify multiple tags x509' '\n     -+#\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+# \ttags=\"seventh-signed ninth-signed-x509\" &&\n     -+# \tfor i in $tags\n     -+# \tdo\n     -+# \t\tgit verify-tag -v --raw $i || return 1\n     -+# \tdone >expect.stdout 2>expect.stderr.1 &&\n     -+# \tgrep \"^.GNUPG:.\" <expect.stderr.1 >expect.stderr &&\n     -+# \tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n     -+# \tgrep \"^.GNUPG:.\" <actual.stderr.1 >actual.stderr &&\n     -+# \ttest_cmp expect.stdout actual.stdout &&\n     -+# \ttest_cmp expect.stderr actual.stderr\n     -+# '\n     -+\n     -+test_expect_success GPGSSH 'verifying tag with --format' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     -+\tcat >expect <<-\\EOF &&\n     -+\ttagname : fourth-signed\n     -+\tEOF\n     -+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently' '\n     -+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n     -+\ttest_must_be_empty actual-forged\n     -+'\n     -+\n     -+test_done\n     -\n       ## t/t7527-signed-commit-ssh.sh (new) ##\n      @@\n      +#!/bin/sh\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +\ttest_must_fail git verify-commit eighth-signed-alt\n      +'\n      +\n     -+# test_expect_success GPGSSH 'verify signatures with --raw' '\n     -+# \t(\n     -+# \t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n     -+# \t\tdo\n     -+# \t\t\tgit verify-commit --raw $commit 2>actual &&\n     -+# \t\t\tgrep \"GOODSIG\" actual &&\n     -+# \t\t\t! grep \"BADSIG\" actual &&\n     -+# \t\t\techo $commit OK || exit 1\n     -+# \t\tdone\n     -+# \t) &&\n     -+# \t(\n     -+# \t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n     -+# \t\tdo\n     -+# \t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n     -+# \t\t\t! grep \"GOODSIG\" actual &&\n     -+# \t\t\t! grep \"BADSIG\" actual &&\n     -+# \t\t\techo $commit OK || exit 1\n     -+# \t\tdone\n     -+# \t) &&\n     -+# \t(\n     -+# \t\tfor commit in eighth-signed-alt\n     -+# \t\tdo\n     -+# \t\t\tgit verify-commit --raw $commit 2>actual &&\n     -+# \t\t\tgrep \"GOODSIG\" actual &&\n     -+# \t\t\t! grep \"BADSIG\" actual &&\n     -+# \t\t\tgrep \"TRUST_UNDEFINED\" actual &&\n     -+# \t\t\techo $commit OK || exit 1\n     -+# \t\tdone\n     -+# \t)\n     -+# '\n     ++test_expect_success GPGSSH 'verify signatures with --raw' '\n     ++\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\t(\n     ++\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n     ++\t\tdo\n     ++\t\t\tgit verify-commit --raw $commit 2>actual &&\n     ++\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\techo $commit OK || exit 1\n     ++\t\tdone\n     ++\t) &&\n     ++\t(\n     ++\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n     ++\t\tdo\n     ++\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n     ++\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\techo $commit OK || exit 1\n     ++\t\tdone\n     ++\t) &&\n     ++\t(\n     ++\t\tfor commit in eighth-signed-alt\n     ++\t\tdo\n     ++\t\t\tgit verify-commit --raw $commit 2>actual &&\n     ++\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\techo $commit OK || exit 1\n     ++\t\tdone\n     ++\t)\n     ++'\n      +\n      +test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n      +\tgit cat-file commit fourth-signed >output &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +\tFINGERPRINT\n      +\tprincipal_1\n      +\tFINGERPRINT\n     -+\t\n     ++\n      +\tEOF\n      +\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n      +\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +\tFINGERPRINT\n      +\n      +\tFINGERPRINT\n     -+\t\n     ++\n      +\tEOF\n      +\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n      +\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +\ttest_must_fail git commit -S --amend -m \"fail\"\n      +'\n      +\n     -+# test_expect_success GPGSSH 'detect fudged commit with double signature' '\n     -+# \tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n     -+# \tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n     -+# \t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n     -+# \tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n     -+# \tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n     -+# \tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n     -+# \t\tdouble-combined.asc > double-gpgsig &&\n     -+# \tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n     -+# \tgit hash-object -w -t commit double-commit >double-commit.commit &&\n     -+# \ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n     -+# \tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n     -+# \tgrep \"BAD signature from\" double-actual &&\n     -+# \tgrep \"Good signature from\" double-actual\n     -+# '\n     ++test_expect_failure GPGSSH 'detect fudged commit with double signature (TODO)' '\n     ++\tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n     ++\tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n     ++\t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n     ++\tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n     ++\tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n     ++\tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n     ++\t\tdouble-combined.asc > double-gpgsig &&\n     ++\tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n     ++\tgit hash-object -w -t commit double-commit >double-commit.commit &&\n     ++\ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n     ++\tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n     ++\tgrep \"BAD signature from\" double-actual &&\n     ++\tgrep \"Good signature from\" double-actual\n     ++'\n      +\n     -+# test_expect_success GPGSSH 'show double signature with custom format' '\n     -+# \tcat >expect <<-\\EOF &&\n     -+# \tE\n     ++test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '\n     ++\tcat >expect <<-\\EOF &&\n     ++\tE\n      +\n      +\n      +\n      +\n     -+# \tEOF\n     -+# \tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n     -+# \ttest_cmp expect actual\n     -+# '\n     ++\tEOF\n     ++\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n      +\n      +\n     -+# test_expect_success GPGSSH 'verify-commit verifies multiply signed commits' '\n     -+# \tgit init multiply-signed &&\n     -+# \tcd multiply-signed &&\n     -+# \ttest_commit first &&\n     -+# \techo 1 >second &&\n     -+# \tgit add second &&\n     -+# \ttree=$(git write-tree) &&\n     -+# \tparent=$(git rev-parse HEAD^{commit}) &&\n     -+# \tgit commit --gpg-sign -m second &&\n     -+# \tgit cat-file commit HEAD &&\n     -+# \t# Avoid trailing whitespace.\n     -+# \tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n     -+# \tQtree $tree\n     -+# \tQparent $parent\n     -+# \tQauthor A U Thor <author@example.com> 1112912653 -0700\n     -+# \tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n     -+# \tQgpgsig -----BEGIN PGP SIGNATURE-----\n     -+# \tQZ\n     -+# \tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n     -+# \tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n     -+# \tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n     -+# \tQ =tQ0N\n     -+# \tQ -----END PGP SIGNATURE-----\n     -+# \tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n     -+# \tQZ\n     -+# \tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n     -+# \tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n     -+# \tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n     -+# \tQ =pIwP\n     -+# \tQ -----END PGP SIGNATURE-----\n     -+# \tQ\n     -+# \tQsecond\n     -+# \tEOF\n     -+# \thead=$(git hash-object -t commit -w commit) &&\n     -+# \tgit reset --hard $head &&\n     -+# \tgit verify-commit $head 2>actual &&\n     -+# \tgrep \"Good signature from\" actual &&\n     -+# \t! grep \"BAD signature from\" actual\n     -+# '\n     ++test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '\n     ++\tgit init multiply-signed &&\n     ++\tcd multiply-signed &&\n     ++\ttest_commit first &&\n     ++\techo 1 >second &&\n     ++\tgit add second &&\n     ++\ttree=$(git write-tree) &&\n     ++\tparent=$(git rev-parse HEAD^{commit}) &&\n     ++\tgit commit --gpg-sign -m second &&\n     ++\tgit cat-file commit HEAD &&\n     ++\t# Avoid trailing whitespace.\n     ++\tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n     ++\tQtree $tree\n     ++\tQparent $parent\n     ++\tQauthor A U Thor <author@example.com> 1112912653 -0700\n     ++\tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n     ++\tQgpgsig -----BEGIN PGP SIGNATURE-----\n     ++\tQZ\n     ++\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n     ++\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n     ++\tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n     ++\tQ =tQ0N\n     ++\tQ -----END PGP SIGNATURE-----\n     ++\tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n     ++\tQZ\n     ++\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n     ++\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n     ++\tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n     ++\tQ =pIwP\n     ++\tQ -----END PGP SIGNATURE-----\n     ++\tQ\n     ++\tQsecond\n     ++\tEOF\n     ++\thead=$(git hash-object -t commit -w commit) &&\n     ++\tgit reset --hard $head &&\n     ++\tgit verify-commit $head 2>actual &&\n     ++\tgrep \"Good signature from\" actual &&\n     ++\t! grep \"BAD signature from\" actual\n     ++'\n      +\n      +test_done\n  -:  ----------- >  9:  33330fda441 ssh signing: add more tests for logs, tags & push certs\n\n-- \ngitgitgadget\n"},{"id":"430027","messageId":"b84b2812470ea45a85d624ec339f35bb0107493d.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:07Z","receivedAt":"2021-07-14T12:10:22Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\ncalls ssh-add -L and uses the first key\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 3c9a48c8e7e..c956ed87475 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -467,6 +467,23 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+/* Returns the first public key from an ssh-agent to use for signing */\n+static char *get_default_ssh_signing_key(void) {\n+\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf key_stdout = STRBUF_INIT;\n+\tstruct strbuf **keys;\n+\n+\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n+\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n+\tif (!ret) {\n+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n+\t\tif (keys[0])\n+\t\t\treturn strbuf_detach(keys[0], NULL);\n+\t}\n+\n+\treturn \"\";\n+}\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n-- \ngitgitgadget\n\n"},{"id":"430029","messageId":"0581c72634cc4c289d7a063ac2c330d2f87e82b3.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 5/9] ssh signing: provide a textual representation of the signing key","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:09Z","receivedAt":"2021-07-14T12:10:25Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nfor ssh the key can be a filename/path or even a literal ssh pubkey\nin push certs and textual output we prefer the ssh fingerprint instead\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++\n gpg-interface.h |  7 +++++++\n send-pack.c     |  8 ++++----\n 3 files changed, 58 insertions(+), 4 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex fa32a57d372..328af86c272 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -467,6 +467,42 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+static char *get_ssh_key_fingerprint(const char *signing_key) {\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n+\tstruct strbuf **fingerprint;\n+\n+\t/*\n+\t * With SSH Signing this can contain a filename or a public key\n+\t * For textual representation we usually want a fingerprint\n+\t */\n+\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n+\t\t\t\t\t\"-lf\", \"-\",\n+\t\t\t\t\tNULL);\n+\t\tret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key),\n+\t\t\t&fingerprint_stdout, 0,  NULL, 0);\n+\t} else {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n+\t\t\t\t\t\"-lf\", configured_signing_key,\n+\t\t\t\t\tNULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n+\t\t\tNULL, 0);\n+\t}\n+\n+\tif (!!ret)\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\tsigning_key);\n+\n+\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n+\tif (!fingerprint[1])\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\tsigning_key);\n+\n+\treturn strbuf_detach(fingerprint[1], NULL);\n+}\n+\n /* Returns the first public key from an ssh-agent to use for signing */\n static char *get_default_ssh_signing_key(void) {\n \tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n@@ -484,6 +520,17 @@ static char *get_default_ssh_signing_key(void) {\n \n \treturn \"\";\n }\n+\n+/* Returns a textual but unique representation ot the signing key */\n+const char *get_signing_key_id(void) {\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_ssh_key_fingerprint(get_signing_key());\n+\t} else {\n+\t\t/* GPG/GPGSM only store a key id on this variable */\n+\t\treturn get_signing_key();\n+\t}\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 5dfd92b81f6..1e842188c26 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -64,6 +64,13 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+\n+/*\n+ * Returns a textual unique representation of the signing key in use\n+ * Either a GPG KeyID or a SSH Key Fingerprint\n+ */\n+const char *get_signing_key_id(void);\n+\n const char *get_ssh_allowed_signers(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\ndiff --git a/send-pack.c b/send-pack.c\nindex 9cb9f716509..191fc6da544 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n {\n \tconst struct ref *ref;\n \tstruct string_list_item *item;\n-\tchar *signing_key = xstrdup(get_signing_key());\n+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n \tconst char *cp, *np;\n \tstruct strbuf cert = STRBUF_INIT;\n \tint update_seen = 0;\n \n \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n \tdatestamp(&cert);\n \tstrbuf_addch(&cert, '\\n');\n \tif (args->url && *args->url) {\n@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, get_signing_key()))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tpacket_buf_write(req_buf, \"push-cert-end\\n\");\n \n free_return:\n-\tfree(signing_key);\n+\tfree(signing_key_id);\n \tstrbuf_release(&cert);\n \treturn update_seen;\n }\n-- \ngitgitgadget\n\n"},{"id":"430030","messageId":"df55b9e1d5989766b08596f5b9057ba0be38c10e.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 4/9] ssh signing: sign using either gpg or ssh keys","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:08Z","receivedAt":"2021-07-14T12:10:25Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nimplements the actual ssh-keygen -Y sign operation\n\nSet gpg.format = ssh and user.signingkey to either a ssh public key\nstring (like from an authorized_keys file), or a ssh key file.\nIf the key file or the config value itself contains only a public key\nthen the private key needs to be available via ssh-agent.\nIf no signingkey is set then git will call 'ssh-add -L' to check for\navailable agent keys and use the first one for signing.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 86 +++++++++++++++++++++++++++++++++++++++++++------\n 1 file changed, 76 insertions(+), 10 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex c956ed87475..fa32a57d372 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -505,30 +505,96 @@ const char *get_ssh_allowed_signers(void)\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n {\n-\tstruct child_process gpg = CHILD_PROCESS_INIT;\n+\tstruct child_process signer = CHILD_PROCESS_INIT;\n \tint ret;\n \tsize_t i, j, bottom;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct strbuf signer_stderr = STRBUF_INIT;\n+\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n+\tchar *ssh_signing_key_file = NULL;\n+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n \n-\tstrvec_pushl(&gpg.args,\n-\t\t     use_format->program,\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\tif (!signing_key || signing_key[0] == '\\0')\n+\t\t\treturn error(_(\"user.signingkey needs to be set for ssh signing\"));\n+\n+\n+\t\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\t\t/* A literal ssh key */\n+\t\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\t\tif (!temp)\n+\t\t\t\treturn error_errno(_(\"could not create temporary file\"));\n+\t\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) < 0 ||\n+\t\t\t\tclose_tempfile_gently(temp) < 0) {\n+\t\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n+\t\t\t\t\ttemp->filename.buf);\n+\t\t\t\tdelete_tempfile(&temp);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t\tssh_signing_key_file= temp->filename.buf;\n+\t\t} else {\n+\t\t\t/* We assume a file */\n+\t\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n+\t\t}\n+\n+\t\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n+\t\tif (!buffer_file)\n+\t\t\treturn error_errno(_(\"could not create temporary file\"));\n+\t\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n+\t\t\tclose_tempfile_gently(buffer_file) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n+\t\t\t\tbuffer_file->filename.buf);\n+\t\t\tdelete_tempfile(&buffer_file);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tstrvec_pushl(&signer.args, use_format->program ,\n+\t\t\t\t\t\"-Y\", \"sign\",\n+\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\"-f\", ssh_signing_key_file,\n+\t\t\t\t\tbuffer_file->filename.buf,\n+\t\t\t\t\tNULL);\n+\n+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n+\t\tsigchain_pop(SIGPIPE);\n+\n+\t\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n+\t\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n+\t\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n+\t\t\terror_errno(_(\"failed reading ssh signing data buffer from '%s'\"),\n+\t\t\t\tssh_signature_filename.buf);\n+\t\t}\n+\t\tunlink_or_warn(ssh_signature_filename.buf);\n+\t\tstrbuf_release(&ssh_signature_filename);\n+\t\tdelete_tempfile(&buffer_file);\n+\t} else {\n+\t\tstrvec_pushl(&signer.args, use_format->program ,\n \t\t     \"--status-fd=2\",\n \t\t     \"-bsau\", signing_key,\n \t\t     NULL);\n \n-\tbottom = signature->len;\n-\n \t/*\n \t * When the username signingkey is bad, program could be terminated\n \t * because gpg exits without reading and then write gets SIGPIPE.\n \t */\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, buffer->buf, buffer->len,\n-\t\t\t   signature, 1024, &gpg_status, 0);\n+\t\tret = pipe_command(&signer, buffer->buf, buffer->len, signature, 1024, &signer_stderr, 0);\n \tsigchain_pop(SIGPIPE);\n+\t}\n+\n+\tbottom = signature->len;\n+\n+\tif (temp)\n+\t\tdelete_tempfile(&temp);\n \n-\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n-\tstrbuf_release(&gpg_status);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\tif (strstr(signer_stderr.buf, \"usage:\")) {\n+\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n+\t\t}\n+\t} else {\n+\t\tret |= !strstr(signer_stderr.buf, \"\\n[GNUPG:] SIG_CREATED \");\n+\t}\n+\tstrbuf_release(&signer_stderr);\n \tif (ret)\n \t\treturn error(_(\"gpg failed to sign the data\"));\n \n-- \ngitgitgadget\n\n"},{"id":"430031","messageId":"381a950a6e1708b3895bb9c9cb46e974e142ae64.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:10Z","receivedAt":"2021-07-14T12:10:27Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nVerification uses the gpg.ssh.keyring file (see ssh-keygen(1) \"ALLOWED\nSIGNERS\") which contains valid public keys and a principal (usually\nuser@domain). Depending on the environment this file can be managed by\nthe individual developer or for example generated by the central\nrepository server from known ssh keys with push access. If the\nrepository only allows signed commits / pushes then the file can even be\nstored inside it.\n\nTo revoke a key put the public key without the principal prefix into\ngpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n\"KEY REVOCATION LISTS\"). The same considerations about who to trust for\nverification as with the keyring file apply.\n\nUsing SSH CA Keys with these files is also possible. Add\n\"cert-authority\" as key option between the principal and the key to mark\nit as a CA and all keys signed by it as valid for this CA.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n builtin/receive-pack.c |   2 +\n gpg-interface.c        | 139 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 141 insertions(+)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513ac..62b11c5f3a4 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n {\n \tint status = parse_hide_refs_config(var, value, \"receive\");\n \n+\tgit_gpg_config(var, value, NULL);\n+\n \tif (status)\n \t\treturn status;\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 328af86c272..1be88b87d96 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -3,6 +3,7 @@\n #include \"config.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n+#include \"dir.h\"\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n@@ -156,6 +157,42 @@ static int parse_gpg_trust_level(const char *level,\n \treturn 1;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tstruct string_list parts = STRING_LIST_INIT_DUP;\n+\tchar *line = NULL;\n+\n+\t/*\n+\t * ssh-keysign output should be:\n+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\t * or for valid but unknown keys:\n+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n+\t */\n+\tsigc->result = 'B';\n+\tsigc->trust_level = TRUST_NEVER;\n+\n+\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n+\tstring_list_split(&parts, line, ' ', 8);\n+\tif (parts.nr >= 9 && starts_with(line, \"Good \\\"git\\\" signature for \")) {\n+\t\t/* Valid signature for a trusted signer */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_FULLY;\n+\t\tsigc->signer = xstrdup(parts.items[4].string);\n+\t\tsigc->fingerprint = xstrdup(parts.items[8].string);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t} else if (parts.nr >= 7 && starts_with(line, \"Good \\\"git\\\" signature with \")) {\n+\t\t/* Valid signature, but key unknown */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_UNDEFINED;\n+\t\tsigc->fingerprint = xstrdup(parts.items[6].string);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t}\n+\ttrace_printf(\"trace: sigc result %c/%d - %s %s %s\", sigc->result, sigc->trust_level, sigc->signer, sigc->fingerprint, sigc->key);\n+\n+\tstring_list_clear(&parts, 0);\n+\tFREE_AND_NULL(line);\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n@@ -269,6 +306,108 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n+static int verify_ssh_signature(struct signature_check *sigc,\n+\tstruct gpg_format *fmt,\n+\tconst char *payload, size_t payload_size,\n+\tconst char *signature, size_t signature_size)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tstruct tempfile *temp;\n+\tint ret;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n+\n+\ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n+\tif (!temp)\n+\t\treturn error_errno(_(\"could not create temporary file\"));\n+\tif (write_in_full(temp->fd, signature, signature_size) < 0 ||\n+\t    close_tempfile_gently(temp) < 0) {\n+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n+\t\t\t    temp->filename.buf);\n+\t\tdelete_tempfile(&temp);\n+\t\treturn -1;\n+\t}\n+\n+\t/* Find the principal from the signers */\n+\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n+\t\t\t\t\t\"-Y\", \"find-principals\",\n+\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n+\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\tNULL);\n+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\tif (strstr(ssh_keygen_err.buf, \"usage:\")) {\n+\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n+\t}\n+\tif (ret || !ssh_keygen_out.len) {\n+\t\t/* We did not find a matching principal in the keyring - Check without validation */\n+\t\tchild_process_init(&ssh_keygen);\n+\t\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n+\t\t\t\t\t\t\"-Y\", \"check-novalidate\",\n+\t\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\t\tNULL);\n+\t\tret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t} else {\n+\t\t/* Check every principal we found (one per line) */\n+\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n+\n+\t\t\ttrust_size = strcspn(line, \" \\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n+\n+\t\t\tchild_process_init(&ssh_keygen);\n+\t\t\tstrbuf_release(&ssh_keygen_out);\n+\t\t\tstrbuf_release(&ssh_keygen_err);\n+\t\t\tstrvec_push(&ssh_keygen.args,fmt->program);\n+\t\t\t/* We found principals - Try with each until we find a match */\n+\t\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"verify\",\n+\t\t\t\t\t\t\t\"-n\", \"git\",\n+\t\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n+\t\t\t\t\t\t\t\"-I\", principal,\n+\t\t\t\t\t\t\t\"-s\", temp->filename.buf,\n+\t\t\t\t\t\t\tNULL);\n+\n+\t\t\tif (ssh_revocation_file) {\n+\t\t\t\tif (file_exists(ssh_revocation_file)) {\n+\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\", ssh_revocation_file, NULL);\n+\t\t\t\t} else {\n+\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"), ssh_revocation_file);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t\t&ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n+\n+\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n+\t\t\tif (ret == 0)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n+\tsigc->gpg_status = xstrdup(sigc->output);\n+\n+\tparse_ssh_output(sigc);\n+\n+\tdelete_tempfile(&temp);\n+\tstrbuf_release(&ssh_keygen_out);\n+\tstrbuf_release(&ssh_keygen_err);\n+\n+\treturn ret;\n+}\n+\n static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,\n \tconst char *payload, size_t payload_size,\n \tconst char *signature, size_t signature_size)\n-- \ngitgitgadget\n\n"},{"id":"430032","messageId":"1d292a8d7a286ff588a9c189ca961f2ab844d723.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 7/9] ssh signing: add test prereqs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:11Z","receivedAt":"2021-07-14T12:10:28Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\ngenerate some ssh keys and a allowed keys keyring for testing\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/lib-gpg.sh | 27 +++++++++++++++++++++++++++\n 1 file changed, 27 insertions(+)\n\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\nindex 9fc5241228e..c65cdde9e5f 100644\n--- a/t/lib-gpg.sh\n+++ b/t/lib-gpg.sh\n@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 '\n \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n '\n \n+test_lazy_prereq GPGSSH '\n+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n+\ttest $? != 127 || exit 1\n+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n+\ttest $? = 0 || exit 1;\n+\tmkdir -p \"${GNUPGHOME}\" &&\n+\tchmod 0700 \"${GNUPGHOME}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n+\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"principal_\\\" NR \\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n+\tcat \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n+'\n+\n+SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n+SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n+SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n+SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n+SIGNING_KEY_PASSPHRASE=\"super_secret\"\n+SIGNING_KEYRING=\"${GNUPGHOME}/ssh.all_valid.keyring\"\n+\n+GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n+GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n+KEY_NOT_TRUSTED=\"No principal matched\"\n+BAD_SIGNATURE=\"Signature verification failed\"\n+\n sanitize_pgp() {\n \tperl -ne '\n \t\t/^-----END PGP/ and $in_pgp = 0;\n-- \ngitgitgadget\n\n"},{"id":"430033","messageId":"338d1b976e92fc3001fceebbc89cd7c41c58b7d6.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 8/9] ssh signing: duplicate t7510 tests for commits","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:12Z","receivedAt":"2021-07-14T12:10:29Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++\n 1 file changed, 398 insertions(+)\n create mode 100755 t/t7527-signed-commit-ssh.sh\n\ndiff --git a/t/t7527-signed-commit-ssh.sh b/t/t7527-signed-commit-ssh.sh\nnew file mode 100755\nindex 00000000000..305b3b9160b\n--- /dev/null\n+++ b/t/t7527-signed-commit-ssh.sh\n@@ -0,0 +1,398 @@\n+#!/bin/sh\n+\n+test_description='ssh signed commit tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed commits' '\n+\ttest_oid_cache <<-\\EOF &&\n+\theader sha1:gpgsig\n+\theader sha256:gpgsig-sha256\n+\tEOF\n+\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -S -m initial &&\n+\tgit tag initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -S -m second &&\n+\tgit tag second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n+\tgit tag fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag fourth-signed &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n+\tgit tag fifth-signed &&\n+\n+\tgit config commit.gpgsign false &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag sixth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n+\tgit tag seventh-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n+\tgit tag seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${SIGNING_KEY_UNTRUSTED}\" &&\n+\tgit tag eighth-signed-alt &&\n+\n+\t# commit.gpgsign is still on but this must not be signed\n+\techo 9 | git commit-tree HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag ninth-unsigned $(cat oid) &&\n+\t# explicit -S of course must sign.\n+\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag tenth-signed $(cat oid) &&\n+\n+\t# --gpg-sign[=<key-id>] must sign.\n+\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag eleventh-signed $(cat oid) &&\n+\techo 12 | git commit-tree --gpg-sign=\"${SIGNING_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag twelfth-signed-alt $(cat oid)\n+'\n+\n+test_expect_success GPGSSH 'verify and show signatures' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed \\\n+\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n+\t\t\televenth-signed\n+\t\tdo\n+\t\t\tgit verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n+\t\t\tseventh-unsigned ninth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n+\t\tdo\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit verify-commit eighth-signed-alt 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\tgrep \"${KEY_NOT_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n+\tgit cat-file commit fourth-signed >output &&\n+\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n+'\n+\n+test_expect_success GPGSSH 'show signed commit with signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit show -s initial >commit &&\n+\tgit show -s --show-signature initial >show &&\n+\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n+\tgit cat-file commit initial >cat &&\n+\tgrep -v -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n+\tgrep -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n+\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n+\ttest_cmp show.commit commit &&\n+\ttest_cmp show.gpg verify.2 &&\n+\ttest_cmp cat.commit verify.1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t commit forged1 >forged1.commit &&\n+\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature with NUL' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tcat raw >forged2 &&\n+\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n+\tgit hash-object -w -t commit forged2 >forged2.commit &&\n+\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual2 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual2\n+'\n+\n+test_expect_success GPGSSH 'amending already signed commit' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit checkout fourth-signed^0 &&\n+\tgit commit --amend -S --no-edit &&\n+\tgit verify-commit HEAD &&\n+\tgit show -s --show-signature HEAD >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual\n+'\n+\n+test_expect_success GPGSSH 'show good signature with custom format' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tG\n+\tFINGERPRINT\n+\tprincipal_1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show bad signature with custom format' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect <<-\\EOF &&\n+\tB\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with custom format' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tU\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tundefined\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tfully\n+\tFINGERPRINT\n+\tprincipal_1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show lack of signature with custom format' '\n+\tcat >expect <<-\\EOF &&\n+\tN\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttest_config log.showsignature true &&\n+\tgit show initial >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'check config gpg.format values' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.format ssh &&\n+\tgit commit -S --amend -m \"success\" &&\n+\ttest_config gpg.format OpEnPgP &&\n+\ttest_must_fail git commit -S --amend -m \"fail\"\n+'\n+\n+test_expect_failure GPGSSH 'detect fudged commit with double signature (TODO)' '\n+\tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n+\tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n+\t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n+\tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n+\tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n+\tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n+\t\tdouble-combined.asc > double-gpgsig &&\n+\tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n+\tgit hash-object -w -t commit double-commit >double-commit.commit &&\n+\ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n+\tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n+\tgrep \"BAD signature from\" double-actual &&\n+\tgrep \"Good signature from\" double-actual\n+'\n+\n+test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '\n+\tcat >expect <<-\\EOF &&\n+\tE\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '\n+\tgit init multiply-signed &&\n+\tcd multiply-signed &&\n+\ttest_commit first &&\n+\techo 1 >second &&\n+\tgit add second &&\n+\ttree=$(git write-tree) &&\n+\tparent=$(git rev-parse HEAD^{commit}) &&\n+\tgit commit --gpg-sign -m second &&\n+\tgit cat-file commit HEAD &&\n+\t# Avoid trailing whitespace.\n+\tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n+\tQtree $tree\n+\tQparent $parent\n+\tQauthor A U Thor <author@example.com> 1112912653 -0700\n+\tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n+\tQgpgsig -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n+\tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n+\tQ =tQ0N\n+\tQ -----END PGP SIGNATURE-----\n+\tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n+\tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n+\tQ =pIwP\n+\tQ -----END PGP SIGNATURE-----\n+\tQ\n+\tQsecond\n+\tEOF\n+\thead=$(git hash-object -t commit -w commit) &&\n+\tgit reset --hard $head &&\n+\tgit verify-commit $head 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"430034","messageId":"33330fda441d85b13f1c5dcc5e42c89cc727715a.1626264613.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v3 9/9] ssh signing: add more tests for logs, tags & push certs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-14T12:10:13Z","receivedAt":"2021-07-14T12:10:31Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t4202-log.sh                   |  23 +++++\n t/t5534-push-signed.sh           | 101 +++++++++++++++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++\n 3 files changed, 285 insertions(+)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 350cfa35936..41767627ad0 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' '\n \tgit commit -S -m signed_commit\n '\n \n+test_expect_success GPGSSH 'setup sshkey signed branch' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n+\tgit checkout -b signed-ssh main &&\n+\techo foo >foo &&\n+\tgit add foo &&\n+\tgit commit -S -m signed_commit\n+'\n+\n test_expect_success GPGSM 'log x509 fingerprint' '\n \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPGSSH 'log ssh key fingerprint' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature' '\n \tgit log --graph --show-signature -n1 signed >actual &&\n \tgrep \"^| gpg: Signature made\" actual &&\n@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' '\n \tgrep \"^| gpgsm: Good signature\" actual\n '\n \n+test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature for merged tag' '\n \ttest_when_finished \"git reset --hard && git checkout main\" &&\n \tgit checkout -b plain main &&\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex bba768f5ded..37c97756032 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal_1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n \t# First, invoke receive-pack with dummy input to obtain its preamble.\n \tprepare_dst &&\n@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n+\ttest_config gpg.format ssh &&\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config user.email hasnokey@nowhere.com &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"\" &&\n+\t(\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git push --signed dst noop ff +noff\n+\t) &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal_1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'failed atomic push does not execute GPG' '\n \tprepare_dst &&\n \tgit -C dst config receive.certnonceseed sekrit &&\ndiff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh\nnew file mode 100755\nindex 00000000000..2148a246385\n--- /dev/null\n+++ b/t/t7031-verify-tag-signed-ssh.sh\n@@ -0,0 +1,161 @@\n+#!/bin/sh\n+\n+test_description='signed tag tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed tags ssh' '\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -m initial &&\n+\tgit tag -s -m initial initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -m second &&\n+\tgit tag -s -m second second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag -s -m merge merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag -s -m fourth fourth-signed &&\n+\n+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n+\tgit tag fifth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag -a -m sixth sixth-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n+\tgit tag -m seventh -s seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth &&\n+\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify and show ssh signatures' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'detect fudged ssh signature' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit cat-file tag seventh-signed >raw &&\n+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tgit verify-tag --raw sixth-signed 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\techo sixth-signed OK\n+'\n+\n+test_expect_success GPGSSH 'verify multiple tags ssh' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\ttags=\"seventh-signed sixth-signed\" &&\n+\tfor i in $tags\n+\tdo\n+\t\tgit verify-tag -v --raw $i || return 1\n+\tdone >expect.stdout 2>expect.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n+\tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n+\ttest_cmp expect.stdout actual.stdout &&\n+\ttest_cmp expect.stderr actual.stderr\n+'\n+\n+test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n+\tcat >expect <<-\\EOF &&\n+\ttagname : fourth-signed\n+\tEOF\n+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently - ssh' '\n+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n+\ttest_must_be_empty actual-forged\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"430097","messageId":"xmqqlf68wyfa.fsf@gitster.g","threadId":"56054","inReplyTo":"390a8f816cda0574cabe49e9f88ae1803142fb51.1626264613.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-14T18:19:37Z","receivedAt":"2021-07-14T18:19:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n> Subject: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys\n\nIf you chose \"ssh signing:\" as the common prefix for the series, use\nit consistently with this step, too.\n\n> Openssh v8.2p1 added some new options to ssh-keygen for signature\n> creation and verification. These allow us to use ssh keys for git\n> signatures easily.\n>\n> Start with adding the new signature format, new config options and\n> rename some fields for consistency.\n\nOK.\n\n> This feature makes git signing much more accessible to the average user.\n> Usually they have a SSH Key for pushing code already. Using it\n> for signing commits allows us to verify not only the transport but the\n> pushed code as well.\n\nDrop this paragraph or at least tone it down.  It may hold true only\naround your immediate circle but it is far from clear and obvious.\nI'd expect more people push over https:// than ssh://.\n\nWe do not really require a new feature to make much more accessible\nfor wide average user---making it just a bit more accessible to\nfolks in your immediate circle is perfectly fine, as long as you are\nnot harming other people ;-)\n\n> In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n> signing/encryption and ssh keys which i think is quite common\n\nUpcase \"I\".\n\n> (at least for the email part). This way we can establish the correct\n> trust for the SSH Keys without setting up a separate GPG Infrastructure\n> (which is still quite painful for users) or implementing x509 signing\n> support for git (which lacks good forwarding mechanisms).\n> Using ssh agent forwarding makes this feature easily usable in todays\n> development environments where code is often checked out in remote VMs / containers.\n> In such a setup the keyring & revocationKeyring can be centrally\n> generated from the x509 CA information and distributed to the users.\n\nAll of the above promises a wonderful new world, but what is left\nunclear is with this step alone how much of the new world we already\ngain.  When you ask others to read and understand your code, please\ngive them a bit more hint to guide them what to expect and where you\nare taking them next. \n\n> diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\n> index 0f66818e0f8..1d7b64fa021 100644\n> --- a/fmt-merge-msg.c\n> +++ b/fmt-merge-msg.c\n> @@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n>  \t\t\tlen = payload.len;\n>  \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n>  \t\t\t\t\t sig.len, &sigc) &&\n> -\t\t\t\t!sigc.gpg_output)\n> +\t\t\t\t!sigc.output)\n>  \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n>  \t\t\telse\n> -\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n> +\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n\nThese are \"rename some fields for consistency\" the proposed log\nmessage promised.  Makes sense, as you are taking the sigc structure\naway from pgp/gpg dependency.\n\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 127aecfc2b0..3c9a48c8e7e 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -8,6 +8,7 @@\n>  #include \"tempfile.h\"\n>  \n>  static char *configured_signing_key;\n> +const char *ssh_allowed_signers, *ssh_revocation_file;\n\nVery likely these want to be file-scope statics?\n\n>  static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n>  \n>  struct gpg_format {\n> @@ -35,6 +36,14 @@ static const char *x509_sigs[] = {\n>  \tNULL\n>  };\n>  \n> +static const char *ssh_verify_args[] = {\n> +\tNULL\n> +};\n\nA blank line is missing from here.\n\n> +static const char *ssh_sigs[] = {\n> +\t\"-----BEGIN SSH SIGNATURE-----\",\n> +\tNULL\n> +};\n> +\n>  static struct gpg_format gpg_format[] = {\n>  \t{ .name = \"openpgp\", .program = \"gpg\",\n>  \t  .verify_args = openpgp_verify_args,\n> @@ -44,6 +53,9 @@ static struct gpg_format gpg_format[] = {\n>  \t  .verify_args = x509_verify_args,\n>  \t  .sigs = x509_sigs\n>  \t},\n> +\t{ .name = \"ssh\", .program = \"ssh-keygen\",\n> +\t  .verify_args = ssh_verify_args,\n> +\t  .sigs = ssh_sigs },\n>  };\n>  \n>  static struct gpg_format *use_format = &gpg_format[0];\n> @@ -72,7 +84,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n>  void signature_check_clear(struct signature_check *sigc)\n>  {\n>  \tFREE_AND_NULL(sigc->payload);\n> -\tFREE_AND_NULL(sigc->gpg_output);\n> +\tFREE_AND_NULL(sigc->output);\n>  \tFREE_AND_NULL(sigc->gpg_status);\n>  \tFREE_AND_NULL(sigc->signer);\n>  \tFREE_AND_NULL(sigc->key);\n> @@ -257,16 +269,15 @@ error:\n>  \tFREE_AND_NULL(sigc->key);\n>  }\n>  \n> -static int verify_signed_buffer(const char *payload, size_t payload_size,\n> -\t\t\t\tconst char *signature, size_t signature_size,\n> -\t\t\t\tstruct strbuf *gpg_output,\n> -\t\t\t\tstruct strbuf *gpg_status)\n> +static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,\n> +\tconst char *payload, size_t payload_size,\n> +\tconst char *signature, size_t signature_size)\n>  {\n\nWhat is this hunk about?  The more generic name \"verify-signed-buffer\"\nis rescinded and gets replaced by a more GPG/PGP specific helper?\n\nYou'd need to help readers a bit more by explaining in the proposed\nlog message that you shifted the boundary of responsibility between\ncheck_signature() and verify_signed_buffer()---it used to be that\nthe latter inspected the signed payload to see if it a valid GPG/PGP\nsignature before doing GPG specific validation, but you want to make\nthe former responsible for calling get_format_by_sig(), so that you\ncan dispatch a totally new backend that sits next to this GPG\nspecific one.\n\n>  \tstruct child_process gpg = CHILD_PROCESS_INIT;\n> -\tstruct gpg_format *fmt;\n>  \tstruct tempfile *temp;\n>  \tint ret;\n> -\tstruct strbuf buf = STRBUF_INIT;\n> +\tstruct strbuf gpg_out = STRBUF_INIT;\n> +\tstruct strbuf gpg_err = STRBUF_INIT;\n>  \n>  \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n>  \tif (!temp)\n> @@ -279,29 +290,28 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>  \t\treturn -1;\n>  \t}\n>  \n> -\tfmt = get_format_by_sig(signature);\n> -\tif (!fmt)\n> -\t\tBUG(\"bad signature '%s'\", signature);\n> -\n>  \tstrvec_push(&gpg.args, fmt->program);\n>  \tstrvec_pushv(&gpg.args, fmt->verify_args);\n>  \tstrvec_pushl(&gpg.args,\n> -\t\t     \"--status-fd=1\",\n> -\t\t     \"--verify\", temp->filename.buf, \"-\",\n> -\t\t     NULL);\n> -\n> -\tif (!gpg_status)\n> -\t\tgpg_status = &buf;\n> +\t\t\t\"--status-fd=1\",\n> +\t\t\t\"--verify\", temp->filename.buf, \"-\",\n> +\t\t\tNULL);\n\nWhat is going on around here?  Ahh, an unnecessary indentation\nchange is fooling the diff and made the patch unreadable.  Sigh...\n\n>  \tsigchain_push(SIGPIPE, SIG_IGN);\n> -\tret = pipe_command(&gpg, payload, payload_size,\n> -\t\t\t   gpg_status, 0, gpg_output, 0);\n> +\tret = pipe_command(&gpg, payload, payload_size, &gpg_out, 0,\n> +\t\t\t\t&gpg_err, 0);\n\nWhat is this change about?  Is it another unnecessary indentation\nchange?  Please make sure you keep distraction to your readers to\nthe minimum.\n\n> @@ -309,35 +319,36 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>  int check_signature(const char *payload, size_t plen, const char *signature,\n>  \tsize_t slen, struct signature_check *sigc)\n>  {\n> -\tstruct strbuf gpg_output = STRBUF_INIT;\n> -\tstruct strbuf gpg_status = STRBUF_INIT;\n> +\tstruct gpg_format *fmt;\n>  \tint status;\n>  \n>  \tsigc->result = 'N';\n>  \tsigc->trust_level = -1;\n>  \n> -\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n> -\t\t\t\t      &gpg_output, &gpg_status);\n> -\tif (status && !gpg_output.len)\n> -\t\tgoto out;\n> -\tsigc->payload = xmemdupz(payload, plen);\n> -\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n> -\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n> -\tparse_gpg_output(sigc);\n> +\tfmt = get_format_by_sig(signature);\n> +\tif (!fmt) {\n> +\t\terror(_(\"bad/incompatible signature '%s'\"), signature);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\tif (!strcmp(fmt->name, \"ssh\")) {\n> +\t\tstatus = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);\n> +\t} else {\n> +\t\tstatus = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);\n> +\t}\n\nOK, so get_format_by_sig() now is used to dispatch to the right\nbackend.  Which sort of makes sense, but ...\n\n * \"ssh\" is the newcomer; it has no right to come before the\n   battle-tested existing one.\n\n * If we are dispatching via \"fmt\" variable, we should add\n   fmt->verify() method to each of these formats, so that we don't\n   have to switch based on the name.\n\nIOW, this part should just be\n\n\tfmt = get_format_by_sig(signature);\n\tif (!fmt)\n\t\treturn error(_(\"...bad signature...\"));\n\tfmt->verify_signature(sigc, fmt, payload, plen, signature, slen);\n\n> +\tif (status && !sigc->output)\n> +\t\treturn !!status;\n> +\n>  \tstatus |= sigc->result != 'G';\n>  \tstatus |= sigc->trust_level < configured_min_trust_level;\n\nBy the way, there is no verify_ssh_signature() function defined at\nthis step [1/9], so this won't compile from the source at all.\nPlease make sure that each step builds and passes tests.\n\nIf I were doing this patch, I probably would NOT do anything related\nto \"ssh\" in this step.  Probably just doing\n\n - rename gpg_* variables to generic names in codepaths that _will_\n   become generic in future steps (like \"check_signature()\"\n   function);\n\n - introduce verify_signature member to the fmt struct;\n\n - hoist get_format_by_sig()'s callsite to check_signature() from\n   its callee.\n\nwould be sufficient amount of work for the first step.  Call that a\npreliminary refactoring and clean-up.\n\nAnd then in the second and subsequent steps, you may start adding\nadditional code to support ssh signing, including the new instance\nof fmt that has verify_ssh_signature() as its verify_signature\nmethod, etc.\n\nIntroducing ssh_allowed_signers and ssh_revocation_file at this step\nis way premature.  Nobody uses them in this step, the code that uses\nthem is already referenced but missing (hence the code does not\nbuild), so they are only there to frustrate readers wondering what\nthey are for and how they will be used.\n\nThanks.\n"},{"id":"430116","messageId":"xmqq35sgwtga.fsf@gitster.g","threadId":"56054","inReplyTo":"2f8452f6570b1811682863441020a6e43fc556c7.1626264613.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/9] ssh signing: add documentation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-14T20:07:01Z","receivedAt":"2021-07-14T20:08:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n> ---\n>  Documentation/config/gpg.txt  | 35 +++++++++++++++++++++++++++++++++--\n>  Documentation/config/user.txt |  6 ++++++\n>  2 files changed, 39 insertions(+), 2 deletions(-)\n>\n> diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\n> index d94025cb368..16af0b0ada8 100644\n> --- a/Documentation/config/gpg.txt\n> +++ b/Documentation/config/gpg.txt\n> @@ -11,13 +11,13 @@ gpg.program::\n>  \n>  gpg.format::\n>  \tSpecifies which key format to use when signing with `--gpg-sign`.\n> -\tDefault is \"openpgp\" and another possible value is \"x509\".\n> +\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n\nMakes sense.\n\n>  gpg.<format>.program::\n>  \tUse this to customize the program used for the signing format you\n>  \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n>  \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n> -\tvalue for `gpg.x509.program` is \"gpgsm\".\n> +\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n\nAgain, makes sense.\n\nOnce the dust settles, we might want to move the hierarchy from\ngpg.* to a more neutral name, with proper backward compatibility\nmigration plan, but there is no need to do so right away.\n\nBelow, I'll ask many questions.  They are mostly not rhetorical and\nquestions that you should anticipate readers of the documentation\nwill ask (hence, you would want to update your documentation in such\na way that future readers will not have to ask for clarification).\n\n> @@ -33,3 +33,34 @@ gpg.minTrustLevel::\n>  * `marginal`\n>  * `fully`\n>  * `ultimate`\n> +\n> +gpg.ssh.keyring::\n> +\tA file containing all valid SSH public signing keys.\n\nIs \"SSH public signing key\" the phrase we want to use here?  At\nfirst glance I mistakenly thought that I maintain a bag of my keys I\nwill use for signing, but from the mention of \"authorized keys\", it\napparently is the other way around, i.e. I have a bag of public keys\nthat I can use to _verify_ signatures other people made.\n\nWhat do we exactly want to convey with the phrase \"all valid\" to our\nreaders?  Even if I have a valid SSH key that I could sign with, if\nyou and your project do not trust me enough, such a valid key of\nmine would not be in your keyring, so the phrase \"all valid keys\" is\nnot all that meaningful without further qualification in the context\nof this sentence.  A file containing ssh public keys, signatures\nmade with which you are willing to accept, or something?\n\n> +\tSimilar to an .ssh/authorized_keys file.\n\nIt is unclear what \"similarity\" is of interest here.  Similar to\nauthorized keys file, meaning that presense of this file allows\nholders of the listed ssh keys to remotely log-in to the repository?\nI somehow doubt that it is what you meant, but then ...?  Did you\nmean \"Uses the same format as .ssh/authorized_keys file\" or\nsomething like that?\n\n> +\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n> +\tIf a signing key is found in this file then the trust level will\n> +\tbe set to \"fully\". Otherwise if the key is not present\n> +\tbut the signature is still valid then the trust level will be \"undefined\".\n\nI tried to look up the \"ALLOWED SIGNERS\" section for details, but\nfailed to find what \"trust level\" is and how trusted \"fully\" level\nis (is there higher or lower trust levels than that???).  Or is the\nnotion of \"trust level\" foreign to ssh signing world and the readers\nare expected to read this description as \"listed ones are treated as\nhaving the same trust level as 'fully' trusted keys in the GPG/PGP\nworld\"?\n\nI suspect that the section is only useful to learn the details of\nwhat the file looks like?  If so, perhaps instead of saying that the\nkeyring file looks similar to authorized-keys, be more direct and\nsay that the keyring file uses the \"ALLOWED SIGNERS\" file format\ndescribed in that manual page (i.e. bypassing the redirection of\nauthorized-keys)?\n\n> +\tThis file can be set to a location outside of the repository\n> +\tand every developer maintains their own trust store.\n> +\tA central repository server could generate this file automatically\n> +\tfrom ssh keys with push\taccess to verify the code against.\n> +\tIn a corporate setting this file is probably generated at a global location\n> +\tfrom some automation that already handles developer ssh keys.\n\nOK.\n\n> +\tA repository that is only allowing signed commits can store the file\n\n\"is only allowing\" -> \"only allows\".\n\n> +\tin the repository itself using a relative path.\n\nIt is unclear relative to what.  Relative to the top-level of the\nworking tree?\n\n> +\tThis way only committers\n> +\twith an already valid key can add or change keys in the keyring.\n\nOK.\n\n> +\tUsing a SSH CA key with the cert-authority option\n> +\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n> +\n> +\tTo revoke a key place the public key without the principal into the\n> +\trevocationKeyring.\n\nAll of the above unfortunately would not format correctly with\nmultiple paragraphs.  The second and subsequent paragraphs are\npreceded by a line with single '+' on it (instead of a blank line)\nand not indented.\n\nMimick the way the entry for \"ssh.variant\" uses multiple paragraphs.\n\n> +gpg.ssh.revocationKeyring::\n> +\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n> +\tSee ssh-keygen(1) for details.\n> +\tIf a public key is found in this file then it will always be treated\n> +\tas having trust level \"never\" and signatures will show as invalid.\n> diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\n> index 59aec7c3aed..b3c2f2c541e 100644\n> --- a/Documentation/config/user.txt\n> +++ b/Documentation/config/user.txt\n> @@ -36,3 +36,9 @@ user.signingKey::\n>  \tcommit, you can override the default selection with this variable.\n>  \tThis option is passed unchanged to gpg's --local-user parameter,\n>  \tso you may specify a key using any method that gpg supports.\n> +\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n> +\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n> +\tcorresponds to the private key used for signing. The private key\n> +\tneeds to be available via ssh-agent. Alternatively it can be set to\n> +\ta file containing a private key directly. If not set git will call\n> +\t\"ssh-add -L\" and try to use the first key available.\n\nThanks.\n"},{"id":"430120","messageId":"xmqqr1g0ve8w.fsf@gitster.g","threadId":"56054","inReplyTo":"b84b2812470ea45a85d624ec339f35bb0107493d.1626264613.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-14T20:20:47Z","receivedAt":"2021-07-14T20:20:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> calls ssh-add -L and uses the first key\n\nDocumentation/SubmittingPatches::[[describe-changes]].\n\n> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n> ---\n>  gpg-interface.c | 17 +++++++++++++++++\n>  1 file changed, 17 insertions(+)\n>\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 3c9a48c8e7e..c956ed87475 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -467,6 +467,23 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n>  \treturn 0;\n>  }\n>  \n> +/* Returns the first public key from an ssh-agent to use for signing */\n> +static char *get_default_ssh_signing_key(void) {\n\nStyle.  Open and close braces around a function sit on their own\nlines by themselves.\n\n> +\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tstruct strbuf key_stdout = STRBUF_INIT;\n> +\tstruct strbuf **keys;\n\nWhose releasing the resource held by \"keys\" when we return?\n\n> +\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n> +\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n\nI often load about half a dozen keys to my ssh-agent so \"ssh-add -L\"\nwill give me multi-line output.  I know you wrote \"the first public\nkey\" above, but that does not mean users who needs to have multiple\nkeys can be limited to use only the first key for signing.  There\nshould be a way to say \"I may have many keys for other reasons, but\nfor signing I want to use this key, not the other ones\".\n\n> +\tif (!ret) {\n> +\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n\nLet's not use strbuf_split_*() that is a horribly wrong interface.\nYou do not want a set of elastic buffer after splitting.  You only\nare peeking the first line, no?  You are leaking keys[] array and\nprobably keys[1], too.\n\n\teol = strchrnul(key_stdout.buf, '\\n');\n\tstrbuf_setlen(&key_stdout, eol - key_stdout.buf);\n\nor something along that line, perhaps?\n\n> +\t\tif (keys[0])\n> +\t\t\treturn strbuf_detach(keys[0], NULL);\n> +\t}\n> +\n> +\treturn \"\";\n> +}\n>  const char *get_signing_key(void)\n\nMissing blank line after the function body.\n\n>  {\n>  \tif (configured_signing_key)\n"},{"id":"430123","messageId":"xmqqmtqovdpg.fsf@gitster.g","threadId":"56054","inReplyTo":"df55b9e1d5989766b08596f5b9057ba0be38c10e.1626264613.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 4/9] ssh signing: sign using either gpg or ssh keys","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-14T20:32:27Z","receivedAt":"2021-07-14T20:32:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n>  {\n> -\tstruct child_process gpg = CHILD_PROCESS_INIT;\n> +\tstruct child_process signer = CHILD_PROCESS_INIT;\n>  \tint ret;\n>  \tsize_t i, j, bottom;\n> -\tstruct strbuf gpg_status = STRBUF_INIT;\n> +\tstruct strbuf signer_stderr = STRBUF_INIT;\n> +\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n> +\tchar *ssh_signing_key_file = NULL;\n> +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n>  \n> -\tstrvec_pushl(&gpg.args,\n> -\t\t     use_format->program,\n> +\tif (!strcmp(use_format->name, \"ssh\")) {\n\nI wonder if we can split the body of these if/else clauses into\nseparate helper functions, point them with fmt structure and\ndispatch via use_format->sign_buffer pointer, just like I suggested\nhow to do the same on the signature validation side.\n\n> +\t\tif (!signing_key || signing_key[0] == '\\0')\n> +\t\t\treturn error(_(\"user.signingkey needs to be set for ssh signing\"));\n> +\n> +\n> +\t\tif (istarts_with(signing_key, \"ssh-\")) {\n> +\t\t\t/* A literal ssh key */\n> +\t\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n> +\t\t\tif (!temp)\n> +\t\t\t\treturn error_errno(_(\"could not create temporary file\"));\n> +\t\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) < 0 ||\n> +\t\t\t\tclose_tempfile_gently(temp) < 0) {\n> +\t\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n> +\t\t\t\t\ttemp->filename.buf);\n> +\t\t\t\tdelete_tempfile(&temp);\n> +\t\t\t\treturn -1;\n> +\t\t\t}\n> +\t\t\tssh_signing_key_file= temp->filename.buf;\n> +\t\t} else {\n> +\t\t\t/* We assume a file */\n> +\t\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n> +\t\t}\n> +\n> +\t\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n> +\t\tif (!buffer_file)\n> +\t\t\treturn error_errno(_(\"could not create temporary file\"));\n> +\t\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n> +\t\t\tclose_tempfile_gently(buffer_file) < 0) {\n> +\t\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n> +\t\t\t\tbuffer_file->filename.buf);\n> +\t\t\tdelete_tempfile(&buffer_file);\n> +\t\t\treturn -1;\n> +\t\t}\n> +\n> +\t\tstrvec_pushl(&signer.args, use_format->program ,\n> +\t\t\t\t\t\"-Y\", \"sign\",\n> +\t\t\t\t\t\"-n\", \"git\",\n> +\t\t\t\t\t\"-f\", ssh_signing_key_file,\n> +\t\t\t\t\tbuffer_file->filename.buf,\n> +\t\t\t\t\tNULL);\n> +\n> +\t\tsigchain_push(SIGPIPE, SIG_IGN);\n> +\t\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n> +\t\tsigchain_pop(SIGPIPE);\n> +\n> +\t\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n> +\t\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n> +\t\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n> +\t\t\terror_errno(_(\"failed reading ssh signing data buffer from '%s'\"),\n> +\t\t\t\tssh_signature_filename.buf);\n> +\t\t}\n> +\t\tunlink_or_warn(ssh_signature_filename.buf);\n> +\t\tstrbuf_release(&ssh_signature_filename);\n> +\t\tdelete_tempfile(&buffer_file);\n> +\t} else {\n> +\t\tstrvec_pushl(&signer.args, use_format->program ,\n>  \t\t     \"--status-fd=2\",\n>  \t\t     \"-bsau\", signing_key,\n>  \t\t     NULL);\n>  \n> -\tbottom = signature->len;\n> -\n>  \t/*\n>  \t * When the username signingkey is bad, program could be terminated\n>  \t * because gpg exits without reading and then write gets SIGPIPE.\n>  \t */\n>  \tsigchain_push(SIGPIPE, SIG_IGN);\n> -\tret = pipe_command(&gpg, buffer->buf, buffer->len,\n> -\t\t\t   signature, 1024, &gpg_status, 0);\n> +\t\tret = pipe_command(&signer, buffer->buf, buffer->len, signature, 1024, &signer_stderr, 0);\n>  \tsigchain_pop(SIGPIPE);\n> +\t}\n> +\n> +\tbottom = signature->len;\n> +\n> +\tif (temp)\n> +\t\tdelete_tempfile(&temp);\n>  \n> -\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n> -\tstrbuf_release(&gpg_status);\n> +\tif (!strcmp(use_format->name, \"ssh\")) {\n> +\t\tif (strstr(signer_stderr.buf, \"usage:\")) {\n> +\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n\nThis looks iffy.  You do call error() to show the error message, but\nyou do not set \"ret\", which affects how the return value from the\nfunction is computed at the end of the function.\n\n> +\t\t}\n> +\t} else {\n> +\t\tret |= !strstr(signer_stderr.buf, \"\\n[GNUPG:] SIG_CREATED \");\n> +\t}\n> +\tstrbuf_release(&signer_stderr);\n\n>  \tif (ret)\n>  \t\treturn error(_(\"gpg failed to sign the data\"));\n\nAnd this error message belongs to the GPG half of the logic, not\nssh (you are allowed to have a separate \"ssh failed to sign\"\nmessage, of course, but the point is that the error message emission\nshould happen in the codepath dispatched for each crypto backend.\n\nAnd of course, again the \"if (ssh) {do this shiny new ssh thing}\nelse {do gpg thing}\" structure is questionable.  We should be\ndispatching with use_format->fn (whatever the method name is), no?\n\nTHanks.\n\n"},{"id":"430160","messageId":"CAPig+cRmSYNgftKeYoHk0hv54rp_bwrEv6zdhLT7QFx+Q34kKQ@mail.gmail.com","threadId":"56054","inReplyTo":"xmqqlf68wyfa.fsf@gitster.g","subject":"Re: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-07-14T23:57:43Z","receivedAt":"2021-07-14T23:57:56Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Jul 14, 2021 at 2:19 PM Junio C Hamano <gitster@pobox.com> wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> > In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n> > signing/encryption and ssh keys which i think is quite common\n>\n> Upcase \"I\".\n\nAlso: s/environemnt/environment/\n"},{"id":"430183","messageId":"CAFQ2z_POEE3F_WPAPy4YRRnZONvsg=MOPmti2YT0me+M7eLFvA@mail.gmail.com","threadId":"56054","inReplyTo":"xmqqr1g0ve8w.fsf@gitster.g","subject":"Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Han-Wen Nienhuys","fromEmail":"hanwen@google.com","sentAt":"2021-07-15T07:49:18Z","receivedAt":"2021-07-15T07:49:33Z","isPatch":true,"sender":{"key":"hanwen@google.com","avatar":"https://avatars.githubusercontent.com/u/31547?v=4"},"body":"On Wed, Jul 14, 2021 at 10:20 PM Junio C Hamano <gitster@pobox.com> wrote:\n> > calls ssh-add -L and uses the first key\n>\n> > +/* Returns the first public key from an ssh-agent to use for signing */\n> > +static char *get_default_ssh_signing_key(void) {\n>\n> Style.  Open and close braces around a function sit on their own\n> lines by themselves.\n\nI recommend using clang-format (there is a config file checked into\nthe tree) which handles most formatting conventions automatically.\n\n-- \nHan-Wen Nienhuys - Google Munich\nI work 80%. Don't expect answers from me on Fridays.\n--\n\nGoogle Germany GmbH, Erika-Mann-Strasse 33, 80636 Munich\n\nRegistergericht und -nummer: Hamburg, HRB 86891\n\nSitz der Gesellschaft: Hamburg\n\nGeschäftsführer: Paul Manicle, Halimah DeLaine Prado\n"},{"id":"430184","messageId":"ddaf5adf-9219-f462-70ee-ce53ac5d3cf9@gigacodes.de","threadId":"56054","inReplyTo":"CAFQ2z_POEE3F_WPAPy4YRRnZONvsg=MOPmti2YT0me+M7eLFvA@mail.gmail.com","subject":"Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-15T08:06:59Z","receivedAt":"2021-07-15T08:07:09Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 15.07.21 09:49, Han-Wen Nienhuys wrote:\n> On Wed, Jul 14, 2021 at 10:20 PM Junio C Hamano <gitster@pobox.com> wrote:\n>>> calls ssh-add -L and uses the first key\n>>> +/* Returns the first public key from an ssh-agent to use for signing */\n>>> +static char *get_default_ssh_signing_key(void) {\n>> Style.  Open and close braces around a function sit on their own\n>> lines by themselves.\n> I recommend using clang-format (there is a config file checked into\n> the tree) which handles most formatting conventions automatically.\nThanks a lot.\nThis should really be in the in the CodingGuidelines and the \nMyFirstContribution docs.\n\nEspecially the clang-format-diff line from its help\n\"git diff -U0 --no-color --relative HEAD^ | clang-format-diff -p1 -i\"\nis incredibly useful. Otherwise people will reformat all the things ^^\n"},{"id":"430185","messageId":"b8cad34e-8969-25f5-1b29-30c60cd27e7b@gigacodes.de","threadId":"56054","inReplyTo":"xmqqr1g0ve8w.fsf@gitster.g","subject":"Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-15T08:13:13Z","receivedAt":"2021-07-15T08:13:20Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 14.07.21 22:20, Junio C Hamano wrote:\n\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> calls ssh-add -L and uses the first key\n> Documentation/SubmittingPatches::[[describe-changes]].\n>\n>> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n>> ---\n>>   gpg-interface.c | 17 +++++++++++++++++\n>>   1 file changed, 17 insertions(+)\n>>\n>> diff --git a/gpg-interface.c b/gpg-interface.c\n>> index 3c9a48c8e7e..c956ed87475 100644\n>> --- a/gpg-interface.c\n>> +++ b/gpg-interface.c\n>> @@ -467,6 +467,23 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n>>   \treturn 0;\n>>   }\n>>   \n>> +/* Returns the first public key from an ssh-agent to use for signing */\n>> +static char *get_default_ssh_signing_key(void) {\n> Style.  Open and close braces around a function sit on their own\n> lines by themselves.\n>> +\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n>> +\tint ret = -1;\n>> +\tstruct strbuf key_stdout = STRBUF_INIT;\n>> +\tstruct strbuf **keys;\n> Whose releasing the resource held by \"keys\" when we return?\n>\n>> +\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n>> +\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n> I often load about half a dozen keys to my ssh-agent so \"ssh-add -L\"\n> will give me multi-line output.  I know you wrote \"the first public\n> key\" above, but that does not mean users who needs to have multiple\n> keys can be limited to use only the first key for signing.  There\n> should be a way to say \"I may have many keys for other reasons, but\n> for signing I want to use this key, not the other ones\".\nI will make the commit message clearer. This function only provides a \ndefault key in case no key is configured in user.signingkey.\nIf you set user.signingkey to a public key the correct private key from \nyour agent will be used for signing.\n>\n>> +\tif (!ret) {\n>> +\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n> Let's not use strbuf_split_*() that is a horribly wrong interface.\n> You do not want a set of elastic buffer after splitting.  You only\n> are peeking the first line, no?  You are leaking keys[] array and\n> probably keys[1], too.\n>\n> \teol = strchrnul(key_stdout.buf, '\\n');\n> \tstrbuf_setlen(&key_stdout, eol - key_stdout.buf);\n>\n> or something along that line, perhaps?\nI have changed it to what you suggested. I'm always a bit hesitant to \nuse arithmetic with string pointers.\nI know its simple and efficient, but IMHO can be hard to read.\n>\n>> +\t\tif (keys[0])\n>> +\t\t\treturn strbuf_detach(keys[0], NULL);\n>> +\t}\n>> +\n>> +\treturn \"\";\n>> +}\n>>   const char *get_signing_key(void)\n> Missing blank line after the function body.\n>\n>>   {\n>>   \tif (configured_signing_key)\n\n-- \nGIGACODES GmbH | Dr. Hermann-Neubauer-Ring 32 | D-63500 Seligenstadt\nwww.gigacodes.de | fs@gigacodes.de\nPhone +49 6182 8955-114 | Fax +49 6182 8955-299 |\nHRB 40711 AG Offenbach a. Main\nGeschäftsführer: Fabian Stelzer | Umsatzsteuer-ID DE219379936\n\n"},{"id":"430186","messageId":"d851e0ed-775c-b2de-ea40-c915781d8869@gigacodes.de","threadId":"56054","inReplyTo":"xmqqlf68wyfa.fsf@gitster.g","subject":"Re: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-15T08:20:14Z","receivedAt":"2021-07-15T08:20:20Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 14.07.21 20:19, Junio C Hamano wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Fabian Stelzer <fs@gigacodes.de>\n>> Subject: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys\n> If you chose \"ssh signing:\" as the common prefix for the series, use\n> it consistently with this step, too.\ndone\n>\n>> Openssh v8.2p1 added some new options to ssh-keygen for signature\n>> creation and verification. These allow us to use ssh keys for git\n>> signatures easily.\n>>\n>> Start with adding the new signature format, new config options and\n>> rename some fields for consistency.\n> OK.\n>\n>> This feature makes git signing much more accessible to the average user.\n>> Usually they have a SSH Key for pushing code already. Using it\n>> for signing commits allows us to verify not only the transport but the\n>> pushed code as well.\n> Drop this paragraph or at least tone it down.  It may hold true only\n> around your immediate circle but it is far from clear and obvious.\n> I'd expect more people push over https:// than ssh://.\n>\n> We do not really require a new feature to make much more accessible\n> for wide average user---making it just a bit more accessible to\n> folks in your immediate circle is perfectly fine, as long as you are\n> not harming other people ;-)\ni will redo the first commit with your suggestions from below only doing \npreperation for the upcoming change and then rewrite the commit message \nto reflect this as well.\n>\n>> In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n>> signing/encryption and ssh keys which i think is quite common\n> Upcase \"I\".\n>\n>> (at least for the email part). This way we can establish the correct\n>> trust for the SSH Keys without setting up a separate GPG Infrastructure\n>> (which is still quite painful for users) or implementing x509 signing\n>> support for git (which lacks good forwarding mechanisms).\n>> Using ssh agent forwarding makes this feature easily usable in todays\n>> development environments where code is often checked out in remote VMs / containers.\n>> In such a setup the keyring & revocationKeyring can be centrally\n>> generated from the x509 CA information and distributed to the users.\n> All of the above promises a wonderful new world, but what is left\n> unclear is with this step alone how much of the new world we already\n> gain.  When you ask others to read and understand your code, please\n> give them a bit more hint to guide them what to expect and where you\n> are taking them next.\n>\n>> diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\n>> index 0f66818e0f8..1d7b64fa021 100644\n>> --- a/fmt-merge-msg.c\n>> +++ b/fmt-merge-msg.c\n>> @@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n>>   \t\t\tlen = payload.len;\n>>   \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n>>   \t\t\t\t\t sig.len, &sigc) &&\n>> -\t\t\t\t!sigc.gpg_output)\n>> +\t\t\t\t!sigc.output)\n>>   \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n>>   \t\t\telse\n>> -\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n>> +\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n> These are \"rename some fields for consistency\" the proposed log\n> message promised.  Makes sense, as you are taking the sigc structure\n> away from pgp/gpg dependency.\n>\n>> diff --git a/gpg-interface.c b/gpg-interface.c\n>> index 127aecfc2b0..3c9a48c8e7e 100644\n>> --- a/gpg-interface.c\n>> +++ b/gpg-interface.c\n>> @@ -8,6 +8,7 @@\n>>   #include \"tempfile.h\"\n>>   \n>>   static char *configured_signing_key;\n>> +const char *ssh_allowed_signers, *ssh_revocation_file;\n> Very likely these want to be file-scope statics?\ntrue\n>\n>>   static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n>>   \n>>   struct gpg_format {\n>> @@ -35,6 +36,14 @@ static const char *x509_sigs[] = {\n>>   \tNULL\n>>   };\n>>   \n>> +static const char *ssh_verify_args[] = {\n>> +\tNULL\n>> +};\n> A blank line is missing from here.\n>\n>> +static const char *ssh_sigs[] = {\n>> +\t\"-----BEGIN SSH SIGNATURE-----\",\n>> +\tNULL\n>> +};\n>> +\n>>   static struct gpg_format gpg_format[] = {\n>>   \t{ .name = \"openpgp\", .program = \"gpg\",\n>>   \t  .verify_args = openpgp_verify_args,\n>> @@ -44,6 +53,9 @@ static struct gpg_format gpg_format[] = {\n>>   \t  .verify_args = x509_verify_args,\n>>   \t  .sigs = x509_sigs\n>>   \t},\n>> +\t{ .name = \"ssh\", .program = \"ssh-keygen\",\n>> +\t  .verify_args = ssh_verify_args,\n>> +\t  .sigs = ssh_sigs },\n>>   };\n>>   \n>>   static struct gpg_format *use_format = &gpg_format[0];\n>> @@ -72,7 +84,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n>>   void signature_check_clear(struct signature_check *sigc)\n>>   {\n>>   \tFREE_AND_NULL(sigc->payload);\n>> -\tFREE_AND_NULL(sigc->gpg_output);\n>> +\tFREE_AND_NULL(sigc->output);\n>>   \tFREE_AND_NULL(sigc->gpg_status);\n>>   \tFREE_AND_NULL(sigc->signer);\n>>   \tFREE_AND_NULL(sigc->key);\n>> @@ -257,16 +269,15 @@ error:\n>>   \tFREE_AND_NULL(sigc->key);\n>>   }\n>>   \n>> -static int verify_signed_buffer(const char *payload, size_t payload_size,\n>> -\t\t\t\tconst char *signature, size_t signature_size,\n>> -\t\t\t\tstruct strbuf *gpg_output,\n>> -\t\t\t\tstruct strbuf *gpg_status)\n>> +static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,\n>> +\tconst char *payload, size_t payload_size,\n>> +\tconst char *signature, size_t signature_size)\n>>   {\n> What is this hunk about?  The more generic name \"verify-signed-buffer\"\n> is rescinded and gets replaced by a more GPG/PGP specific helper?\n>\n> You'd need to help readers a bit more by explaining in the proposed\n> log message that you shifted the boundary of responsibility between\n> check_signature() and verify_signed_buffer()---it used to be that\n> the latter inspected the signed payload to see if it a valid GPG/PGP\n> signature before doing GPG specific validation, but you want to make\n> the former responsible for calling get_format_by_sig(), so that you\n> can dispatch a totally new backend that sits next to this GPG\n> specific one.\n>\n>>   \tstruct child_process gpg = CHILD_PROCESS_INIT;\n>> -\tstruct gpg_format *fmt;\n>>   \tstruct tempfile *temp;\n>>   \tint ret;\n>> -\tstruct strbuf buf = STRBUF_INIT;\n>> +\tstruct strbuf gpg_out = STRBUF_INIT;\n>> +\tstruct strbuf gpg_err = STRBUF_INIT;\n>>   \n>>   \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n>>   \tif (!temp)\n>> @@ -279,29 +290,28 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>>   \t\treturn -1;\n>>   \t}\n>>   \n>> -\tfmt = get_format_by_sig(signature);\n>> -\tif (!fmt)\n>> -\t\tBUG(\"bad signature '%s'\", signature);\n>> -\n>>   \tstrvec_push(&gpg.args, fmt->program);\n>>   \tstrvec_pushv(&gpg.args, fmt->verify_args);\n>>   \tstrvec_pushl(&gpg.args,\n>> -\t\t     \"--status-fd=1\",\n>> -\t\t     \"--verify\", temp->filename.buf, \"-\",\n>> -\t\t     NULL);\n>> -\n>> -\tif (!gpg_status)\n>> -\t\tgpg_status = &buf;\n>> +\t\t\t\"--status-fd=1\",\n>> +\t\t\t\"--verify\", temp->filename.buf, \"-\",\n>> +\t\t\tNULL);\n> What is going on around here?  Ahh, an unnecessary indentation\n> change is fooling the diff and made the patch unreadable.  Sigh...\n>\n>>   \tsigchain_push(SIGPIPE, SIG_IGN);\n>> -\tret = pipe_command(&gpg, payload, payload_size,\n>> -\t\t\t   gpg_status, 0, gpg_output, 0);\n>> +\tret = pipe_command(&gpg, payload, payload_size, &gpg_out, 0,\n>> +\t\t\t\t&gpg_err, 0);\n> What is this change about?  Is it another unnecessary indentation\n> change?  Please make sure you keep distraction to your readers to\n> the minimum.\n>\n>> @@ -309,35 +319,36 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>>   int check_signature(const char *payload, size_t plen, const char *signature,\n>>   \tsize_t slen, struct signature_check *sigc)\n>>   {\n>> -\tstruct strbuf gpg_output = STRBUF_INIT;\n>> -\tstruct strbuf gpg_status = STRBUF_INIT;\n>> +\tstruct gpg_format *fmt;\n>>   \tint status;\n>>   \n>>   \tsigc->result = 'N';\n>>   \tsigc->trust_level = -1;\n>>   \n>> -\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n>> -\t\t\t\t      &gpg_output, &gpg_status);\n>> -\tif (status && !gpg_output.len)\n>> -\t\tgoto out;\n>> -\tsigc->payload = xmemdupz(payload, plen);\n>> -\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n>> -\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n>> -\tparse_gpg_output(sigc);\n>> +\tfmt = get_format_by_sig(signature);\n>> +\tif (!fmt) {\n>> +\t\terror(_(\"bad/incompatible signature '%s'\"), signature);\n>> +\t\treturn -1;\n>> +\t}\n>> +\n>> +\tif (!strcmp(fmt->name, \"ssh\")) {\n>> +\t\tstatus = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);\n>> +\t} else {\n>> +\t\tstatus = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);\n>> +\t}\n> OK, so get_format_by_sig() now is used to dispatch to the right\n> backend.  Which sort of makes sense, but ...\n>\n>   * \"ssh\" is the newcomer; it has no right to come before the\n>     battle-tested existing one.\n>\n>   * If we are dispatching via \"fmt\" variable, we should add\n>     fmt->verify() method to each of these formats, so that we don't\n>     have to switch based on the name.\n>\n> IOW, this part should just be\n>\n> \tfmt = get_format_by_sig(signature);\n> \tif (!fmt)\n> \t\treturn error(_(\"...bad signature...\"));\n> \tfmt->verify_signature(sigc, fmt, payload, plen, signature, slen);\ni did put ssh first to keep the default with gpg and only needing to \nmatch the new format. But the fmt->fn variant is much better. For \nsigning as well.\n>   \n>> +\tif (status && !sigc->output)\n>> +\t\treturn !!status;\n>> +\n>>   \tstatus |= sigc->result != 'G';\n>>   \tstatus |= sigc->trust_level < configured_min_trust_level;\n> By the way, there is no verify_ssh_signature() function defined at\n> this step [1/9], so this won't compile from the source at all.\n> Please make sure that each step builds and passes tests.\nI was thinking about this one when i split up the patch. I was not sure \nif that was required and didn't want to add synthetic changes (that \nwould only appear between the diffs) just for the split.\n>\n> If I were doing this patch, I probably would NOT do anything related\n> to \"ssh\" in this step.  Probably just doing\n>\n>   - rename gpg_* variables to generic names in codepaths that _will_\n>     become generic in future steps (like \"check_signature()\"\n>     function);\n>\n>   - introduce verify_signature member to the fmt struct;\n>\n>   - hoist get_format_by_sig()'s callsite to check_signature() from\n>     its callee.\n>\n> would be sufficient amount of work for the first step.  Call that a\n> preliminary refactoring and clean-up.\n>\n> And then in the second and subsequent steps, you may start adding\n> additional code to support ssh signing, including the new instance\n> of fmt that has verify_ssh_signature() as its verify_signature\n> method, etc.\n>\n> Introducing ssh_allowed_signers and ssh_revocation_file at this step\n> is way premature.  Nobody uses them in this step, the code that uses\n> them is already referenced but missing (hence the code does not\n> build), so they are only there to frustrate readers wondering what\n> they are for and how they will be used.\n>\n> Thanks.\n"},{"id":"430188","messageId":"50eca062-f2e7-28d7-09ae-97250f620be4@gigacodes.de","threadId":"56054","inReplyTo":"xmqqmtqovdpg.fsf@gitster.g","subject":"Re: [PATCH v3 4/9] ssh signing: sign using either gpg or ssh keys","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-15T08:28:33Z","receivedAt":"2021-07-15T08:28:39Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 14.07.21 22:32, Junio C Hamano wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>>   int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n>>   {\n>> -\tstruct child_process gpg = CHILD_PROCESS_INIT;\n>> +\tstruct child_process signer = CHILD_PROCESS_INIT;\n>>   \tint ret;\n>>   \tsize_t i, j, bottom;\n>> -\tstruct strbuf gpg_status = STRBUF_INIT;\n>> +\tstruct strbuf signer_stderr = STRBUF_INIT;\n>> +\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n>> +\tchar *ssh_signing_key_file = NULL;\n>> +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n>>   \n>> -\tstrvec_pushl(&gpg.args,\n>> -\t\t     use_format->program,\n>> +\tif (!strcmp(use_format->name, \"ssh\")) {\n> I wonder if we can split the body of these if/else clauses into\n> separate helper functions, point them with fmt structure and\n> dispatch via use_format->sign_buffer pointer, just like I suggested\n> how to do the same on the signature validation side.\nyes, i like the idea and will do that.\n>\n>> +\t\tif (!signing_key || signing_key[0] == '\\0')\n>> +\t\t\treturn error(_(\"user.signingkey needs to be set for ssh signing\"));\n>> +\n>> +\n>> +\t\tif (istarts_with(signing_key, \"ssh-\")) {\n>> +\t\t\t/* A literal ssh key */\n>> +\t\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n>> +\t\t\tif (!temp)\n>> +\t\t\t\treturn error_errno(_(\"could not create temporary file\"));\n>> +\t\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) < 0 ||\n>> +\t\t\t\tclose_tempfile_gently(temp) < 0) {\n>> +\t\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n>> +\t\t\t\t\ttemp->filename.buf);\n>> +\t\t\t\tdelete_tempfile(&temp);\n>> +\t\t\t\treturn -1;\n>> +\t\t\t}\n>> +\t\t\tssh_signing_key_file= temp->filename.buf;\n>> +\t\t} else {\n>> +\t\t\t/* We assume a file */\n>> +\t\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n>> +\t\t}\n>> +\n>> +\t\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n>> +\t\tif (!buffer_file)\n>> +\t\t\treturn error_errno(_(\"could not create temporary file\"));\n>> +\t\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n>> +\t\t\tclose_tempfile_gently(buffer_file) < 0) {\n>> +\t\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n>> +\t\t\t\tbuffer_file->filename.buf);\n>> +\t\t\tdelete_tempfile(&buffer_file);\n>> +\t\t\treturn -1;\n>> +\t\t}\n>> +\n>> +\t\tstrvec_pushl(&signer.args, use_format->program ,\n>> +\t\t\t\t\t\"-Y\", \"sign\",\n>> +\t\t\t\t\t\"-n\", \"git\",\n>> +\t\t\t\t\t\"-f\", ssh_signing_key_file,\n>> +\t\t\t\t\tbuffer_file->filename.buf,\n>> +\t\t\t\t\tNULL);\n>> +\n>> +\t\tsigchain_push(SIGPIPE, SIG_IGN);\n>> +\t\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n>> +\t\tsigchain_pop(SIGPIPE);\n>> +\n>> +\t\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n>> +\t\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n>> +\t\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n>> +\t\t\terror_errno(_(\"failed reading ssh signing data buffer from '%s'\"),\n>> +\t\t\t\tssh_signature_filename.buf);\n>> +\t\t}\n>> +\t\tunlink_or_warn(ssh_signature_filename.buf);\n>> +\t\tstrbuf_release(&ssh_signature_filename);\n>> +\t\tdelete_tempfile(&buffer_file);\n>> +\t} else {\n>> +\t\tstrvec_pushl(&signer.args, use_format->program ,\n>>   \t\t     \"--status-fd=2\",\n>>   \t\t     \"-bsau\", signing_key,\n>>   \t\t     NULL);\n>>   \n>> -\tbottom = signature->len;\n>> -\n>>   \t/*\n>>   \t * When the username signingkey is bad, program could be terminated\n>>   \t * because gpg exits without reading and then write gets SIGPIPE.\n>>   \t */\n>>   \tsigchain_push(SIGPIPE, SIG_IGN);\n>> -\tret = pipe_command(&gpg, buffer->buf, buffer->len,\n>> -\t\t\t   signature, 1024, &gpg_status, 0);\n>> +\t\tret = pipe_command(&signer, buffer->buf, buffer->len, signature, 1024, &signer_stderr, 0);\n>>   \tsigchain_pop(SIGPIPE);\n>> +\t}\n>> +\n>> +\tbottom = signature->len;\n>> +\n>> +\tif (temp)\n>> +\t\tdelete_tempfile(&temp);\n>>   \n>> -\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n>> -\tstrbuf_release(&gpg_status);\n>> +\tif (!strcmp(use_format->name, \"ssh\")) {\n>> +\t\tif (strstr(signer_stderr.buf, \"usage:\")) {\n>> +\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n> This looks iffy.  You do call error() to show the error message, but\n> you do not set \"ret\", which affects how the return value from the\n> function is computed at the end of the function.\nI can of course fix the ret logic, but i'm not happy with this check in \ngeneral either :/\nThe problem is that ssh-keygen seems to give different error messages \nespecially in between the versions when the command was added (8.1 -> \n8.2) and mac os x has one of those by default. The check in the \nt/lib-gpg.sh is much safer, but requires an additional call to \nssh-keygen which i wanted to avoid here.\n>\n>> +\t\t}\n>> +\t} else {\n>> +\t\tret |= !strstr(signer_stderr.buf, \"\\n[GNUPG:] SIG_CREATED \");\n>> +\t}\n>> +\tstrbuf_release(&signer_stderr);\n>>   \tif (ret)\n>>   \t\treturn error(_(\"gpg failed to sign the data\"));\n> And this error message belongs to the GPG half of the logic, not\n> ssh (you are allowed to have a separate \"ssh failed to sign\"\n> message, of course, but the point is that the error message emission\n> should happen in the codepath dispatched for each crypto backend.\n>\n> And of course, again the \"if (ssh) {do this shiny new ssh thing}\n> else {do gpg thing}\" structure is questionable.  We should be\n> dispatching with use_format->fn (whatever the method name is), no?\n  I will rewrite this with the fmt->fn logic.\n>\n> THanks.\n>\n-- \nGIGACODES GmbH | Dr. Hermann-Neubauer-Ring 32 | D-63500 Seligenstadt\nwww.gigacodes.de | fs@gigacodes.de\nPhone +49 6182 8955-114 | Fax +49 6182 8955-299 |\nHRB 40711 AG Offenbach a. Main\nGeschäftsführer: Fabian Stelzer | Umsatzsteuer-ID DE219379936\n\n"},{"id":"430189","messageId":"cf9aaa48-ea49-e3c2-9909-486d9a3f7aac@gigacodes.de","threadId":"56054","inReplyTo":"xmqq35sgwtga.fsf@gitster.g","subject":"Re: [PATCH v3 2/9] ssh signing: add documentation","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-15T08:48:29Z","receivedAt":"2021-07-15T08:48:35Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 14.07.21 22:07, Junio C Hamano wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n>> ---\n>>   Documentation/config/gpg.txt  | 35 +++++++++++++++++++++++++++++++++--\n>>   Documentation/config/user.txt |  6 ++++++\n>>   2 files changed, 39 insertions(+), 2 deletions(-)\n>>\n>> diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\n>> index d94025cb368..16af0b0ada8 100644\n>> --- a/Documentation/config/gpg.txt\n>> +++ b/Documentation/config/gpg.txt\n>> @@ -11,13 +11,13 @@ gpg.program::\n>>   \n>>   gpg.format::\n>>   \tSpecifies which key format to use when signing with `--gpg-sign`.\n>> -\tDefault is \"openpgp\" and another possible value is \"x509\".\n>> +\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n> Makes sense.\n>\n>>   gpg.<format>.program::\n>>   \tUse this to customize the program used for the signing format you\n>>   \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n>>   \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n>> -\tvalue for `gpg.x509.program` is \"gpgsm\".\n>> +\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n> Again, makes sense.\n>\n> Once the dust settles, we might want to move the hierarchy from\n> gpg.* to a more neutral name, with proper backward compatibility\n> migration plan, but there is no need to do so right away.\n>\n> Below, I'll ask many questions.  They are mostly not rhetorical and\n> questions that you should anticipate readers of the documentation\n> will ask (hence, you would want to update your documentation in such\n> a way that future readers will not have to ask for clarification).\n>\n>> @@ -33,3 +33,34 @@ gpg.minTrustLevel::\n>>   * `marginal`\n>>   * `fully`\n>>   * `ultimate`\n>> +\n>> +gpg.ssh.keyring::\n>> +\tA file containing all valid SSH public signing keys.\n> Is \"SSH public signing key\" the phrase we want to use here?  At\n> first glance I mistakenly thought that I maintain a bag of my keys I\n> will use for signing, but from the mention of \"authorized keys\", it\n> apparently is the other way around, i.e. I have a bag of public keys\n> that I can use to _verify_ signatures other people made.\n>\n> What do we exactly want to convey with the phrase \"all valid\" to our\n> readers?  Even if I have a valid SSH key that I could sign with, if\n> you and your project do not trust me enough, such a valid key of\n> mine would not be in your keyring, so the phrase \"all valid keys\" is\n> not all that meaningful without further qualification in the context\n> of this sentence.  A file containing ssh public keys, signatures\n> made with which you are willing to accept, or something?\nmaybe keeeping the name \"allowedSignersFile\" like its called in the ssh \nmanpage will make this clearer without needing a lot of extra explanation?\nThe keyring name was suggested earlier to make this consistent with gpg. \nBut it really is something different from a gpg keyring.\n>\n>> +\tSimilar to an .ssh/authorized_keys file.\n> It is unclear what \"similarity\" is of interest here.  Similar to\n> authorized keys file, meaning that presense of this file allows\n> holders of the listed ssh keys to remotely log-in to the repository?\n> I somehow doubt that it is what you meant, but then ...?  Did you\n> mean \"Uses the same format as .ssh/authorized_keys file\" or\n> something like that?\nI meant that the format is really similar but i see the problem. I \nwanted to explain the format (which pretty much is just one ssh pubkey \nper line with a name prefixed to identify the key with) so that users \ndon't have to look into the ssh manpage for a basic example. Maybe just \nprovide a short example of the file contents?\n>\n>> +\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n>> +\tIf a signing key is found in this file then the trust level will\n>> +\tbe set to \"fully\". Otherwise if the key is not present\n>> +\tbut the signature is still valid then the trust level will be \"undefined\".\n> I tried to look up the \"ALLOWED SIGNERS\" section for details, but\n> failed to find what \"trust level\" is and how trusted \"fully\" level\n> is (is there higher or lower trust levels than that???).  Or is the\n> notion of \"trust level\" foreign to ssh signing world and the readers\n> are expected to read this description as \"listed ones are treated as\n> having the same trust level as 'fully' trusted keys in the GPG/PGP\n> world\"?\nSSH has nothing compared to the gpg trust levels. Your key is either in \nthe allowed signers file or it is not. However even if it is not in the \nfile then the signature might still be \"Good\" but has no matching \nprincipal to it. To be able to differentiate the two \"Good\" cases i used \nthe existing gpg trust levels. This way if you set gpg.mintrustlevel = \nfully then the signatures with no matching key in the allowed signers \nfile will fail to verify. Otherwise they will verify but show a message \nthat no principal matched with this key.\n>\n> I suspect that the section is only useful to learn the details of\n> what the file looks like?  If so, perhaps instead of saying that the\n> keyring file looks similar to authorized-keys, be more direct and\n> say that the keyring file uses the \"ALLOWED SIGNERS\" file format\n> described in that manual page (i.e. bypassing the redirection of\n> authorized-keys)?\n>\n>> +\tThis file can be set to a location outside of the repository\n>> +\tand every developer maintains their own trust store.\n>> +\tA central repository server could generate this file automatically\n>> +\tfrom ssh keys with push\taccess to verify the code against.\n>> +\tIn a corporate setting this file is probably generated at a global location\n>> +\tfrom some automation that already handles developer ssh keys.\n> OK.\n>\n>> +\tA repository that is only allowing signed commits can store the file\n> \"is only allowing\" -> \"only allows\".\n>\n>> +\tin the repository itself using a relative path.\n> It is unclear relative to what.  Relative to the top-level of the\n> working tree?\n>\n>> +\tThis way only committers\n>> +\twith an already valid key can add or change keys in the keyring.\n> OK.\n>\n>> +\tUsing a SSH CA key with the cert-authority option\n>> +\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n>> +\n>> +\tTo revoke a key place the public key without the principal into the\n>> +\trevocationKeyring.\n> All of the above unfortunately would not format correctly with\n> multiple paragraphs.  The second and subsequent paragraphs are\n> preceded by a line with single '+' on it (instead of a blank line)\n> and not indented.\n>\n> Mimick the way the entry for \"ssh.variant\" uses multiple paragraphs.\nI'll take a look, thanks.\n>\n>> +gpg.ssh.revocationKeyring::\n>> +\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n>> +\tSee ssh-keygen(1) for details.\n>> +\tIf a public key is found in this file then it will always be treated\n>> +\tas having trust level \"never\" and signatures will show as invalid.\n>> diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\n>> index 59aec7c3aed..b3c2f2c541e 100644\n>> --- a/Documentation/config/user.txt\n>> +++ b/Documentation/config/user.txt\n>> @@ -36,3 +36,9 @@ user.signingKey::\n>>   \tcommit, you can override the default selection with this variable.\n>>   \tThis option is passed unchanged to gpg's --local-user parameter,\n>>   \tso you may specify a key using any method that gpg supports.\n>> +\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n>> +\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n>> +\tcorresponds to the private key used for signing. The private key\n>> +\tneeds to be available via ssh-agent. Alternatively it can be set to\n>> +\ta file containing a private key directly. If not set git will call\n>> +\t\"ssh-add -L\" and try to use the first key available.\n> Thanks.\n\n-- \nGIGACODES GmbH | Dr. Hermann-Neubauer-Ring 32 | D-63500 Seligenstadt\nwww.gigacodes.de | fs@gigacodes.de\nPhone +49 6182 8955-114 | Fax +49 6182 8955-299 |\nHRB 40711 AG Offenbach a. Main\nGeschäftsführer: Fabian Stelzer | Umsatzsteuer-ID DE219379936\n\n"},{"id":"430195","messageId":"3fb410ef-a4f7-4f54-27e3-9d468cfeb1bc@gmail.com","threadId":"56054","inReplyTo":"cf9aaa48-ea49-e3c2-9909-486d9a3f7aac@gigacodes.de","subject":"Re: [PATCH v3 2/9] ssh signing: add documentation","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-07-15T10:43:36Z","receivedAt":"2021-07-15T10:43:44Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 15/07/21 15.48, Fabian Stelzer wrote:\n> I meant that the format is really similar but i see the problem. I \n> wanted to explain the format (which pretty much is just one ssh pubkey \n> per line with a name prefixed to identify the key with) so that users \n> don't have to look into the ssh manpage for a basic example. Maybe just \n> provide a short example of the file contents?\n\nYou can write full format description in git-allowedsigners(5) manpage, \nalong with examples of course.\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"430232","messageId":"xmqqfswftuax.fsf@gitster.g","threadId":"56054","inReplyTo":"cf9aaa48-ea49-e3c2-9909-486d9a3f7aac@gigacodes.de","subject":"Re: [PATCH v3 2/9] ssh signing: add documentation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-15T16:29:10Z","receivedAt":"2021-07-15T16:29:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n>>> +gpg.ssh.keyring::\n>>> ...\n> maybe keeeping the name \"allowedSignersFile\" like its called in the\n> ssh manpage will make this clearer without needing a lot of extra\n> explanation?\n\nYup, that sounds like an excellent way to present this to our users.\n\n> SSH has nothing compared to the gpg trust levels. Your key is either\n> in the allowed signers file or it is not. However even if it is not in\n> the file then the signature might still be \"Good\" but has no matching \n> principal to it. To be able to differentiate the two \"Good\" cases i\n> used the existing gpg trust levels. This way if you set\n> gpg.mintrustlevel = fully then the signatures with no matching key in\n> the allowed signers file will fail to verify. Otherwise they will\n> verify but show a message that no principal matched with this key.\n\nSounds sensible.  Our task is to make sure that readers (not me, who\nhave already been spoon-fed the answer by you just now) would reach\nthe above understanding by just reading what we put in the\ndocumentation.\n\nThanks.\n"},{"id":"430286","messageId":"YPDN3Lkg9xm0WCSP@tilde.club","threadId":"56054","inReplyTo":"381a950a6e1708b3895bb9c9cb46e974e142ae64.1626264613.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Gwyneth Morgan","fromEmail":"gwymor@tilde.club","sentAt":"2021-07-16T00:07:56Z","receivedAt":"2021-07-16T00:16:44Z","isPatch":true,"sender":{"key":"gwymor@tilde.club","avatar":"https://avatars.githubusercontent.com/u/87623694?v=4"},"body":"On 2021-07-14 12:10:10+0000, Fabian Stelzer via GitGitGadget wrote:\n> +\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n> +\t\t\twhile (*line == '\\n')\n> +\t\t\t\tline++;\n> +\t\t\tif (!*line)\n> +\t\t\t\tbreak;\n> +\n> +\t\t\ttrust_size = strcspn(line, \" \\n\");\n> +\t\t\tprincipal = xmemdupz(line, trust_size);\n\nThis breaks on principals with spaces in them (principals in the allowed\nsigners file can have spaces if surrounded by quotes). Looks like\nstrcspn should reject \"\\n\" instead of \" \\n\".\n\nBTW, thanks for working on this feature. It seems much more convenient\nthan GPG in my testing.\n"},{"id":"430303","messageId":"3d8a3221-5d28-5707-0b80-5c8a58cc23bb@gigacodes.de","threadId":"56054","inReplyTo":"YPDN3Lkg9xm0WCSP@tilde.club","subject":"Re: [PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-16T07:00:25Z","receivedAt":"2021-07-16T07:00:34Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 16.07.21 02:07, Gwyneth Morgan wrote:\n> On 2021-07-14 12:10:10+0000, Fabian Stelzer via GitGitGadget wrote:\n>> +\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n>> +\t\t\twhile (*line == '\\n')\n>> +\t\t\t\tline++;\n>> +\t\t\tif (!*line)\n>> +\t\t\t\tbreak;\n>> +\n>> +\t\t\ttrust_size = strcspn(line, \" \\n\");\n>> +\t\t\tprincipal = xmemdupz(line, trust_size);\n> This breaks on principals with spaces in them (principals in the allowed\n> signers file can have spaces if surrounded by quotes). Looks like\n> strcspn should reject \"\\n\" instead of \" \\n\".\n>\n> BTW, thanks for working on this feature. It seems much more convenient\n> than GPG in my testing.\nOh thanks. Very nice catch. Easily fixed here but i'll have to rewrite \nthe verification output parsing to account for this as well.\nI will add a testcase too.\n"},{"id":"430494","messageId":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v3.git.git.1626264613.gitgitgadget@gmail.com","subject":"[PATCH v4 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:07Z","receivedAt":"2021-07-19T13:33:23Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"I have added support for using keyfiles directly, lots of tests and\ngenerally cleaned up the signing & verification code a lot.\n\nI can still rename things from being gpg specific to a more general\n\"signing\" but thats rather cosmetic. Also i'm not sure if i named the new\ntest files correctly.\n\nThere is a patch in the pipeline for openssh by Damien Miller that will add\nvalid-after, valid-before options to the allowed keys keyring. This allows\nus to pass the commit timestamp to the verification call and make key\nrollover possible and still be able to verify older commits. Set\nvalid-after=NOW when adding your key to the keyring and set valid-before to\nmake it fail if used after a certain date. Software like gitolite/github or\ncorporate automation can do this automatically when ssh push keys are addded\n/ removed\n\nv3 addresses some issues & refactoring and splits the large commit into\nseveral smaller ones.\n\nv4:\n\n * restructures and cleans up the whole patch set - patches build on its own\n   now and commit messages try to explain whats going on\n * got rid of the if branches and used callback functions in the format\n   struct\n * fixed a bug with whitespace in principal identifiers that required a\n   rewrite of the parse_ssh_output function\n * rewrote documentation to be more clear - also renamed keyring back to\n   allowedSignersFile\n\nanother thing we could add later (via a config switch) is to use the\ncommitter email as principal, instead of looking it up with the key that was\nused to sign, to allow only specific trusted keys per committer.\n\nFabian Stelzer (9):\n  ssh signing: preliminary refactoring and clean-up\n  ssh signing: add ssh signature format and signing using ssh keys\n  ssh signing: retrieve a default key from ssh-agent\n  ssh signing: provide a textual representation of the signing key\n  ssh signing: parse ssh-keygen output and verify signatures\n  ssh signing: add test prereqs\n  ssh signing: duplicate t7510 tests for commits\n  ssh signing: add more tests for logs, tags & push certs\n  ssh signing: add documentation\n\n Documentation/config/gpg.txt     |  39 ++-\n Documentation/config/user.txt    |   6 +\n builtin/receive-pack.c           |   2 +\n fmt-merge-msg.c                  |   6 +-\n gpg-interface.c                  | 485 +++++++++++++++++++++++++++----\n gpg-interface.h                  |   8 +-\n log-tree.c                       |   8 +-\n pretty.c                         |   4 +-\n send-pack.c                      |   8 +-\n t/lib-gpg.sh                     |  27 ++\n t/t4202-log.sh                   |  23 ++\n t/t5534-push-signed.sh           | 101 +++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++\n t/t7527-signed-commit-ssh.sh     | 398 +++++++++++++++++++++++++\n 14 files changed, 1211 insertions(+), 65 deletions(-)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n create mode 100755 t/t7527-signed-commit-ssh.sh\n\n\nbase-commit: 75ae10bc75336db031ee58d13c5037b929235912\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v4\nPull-Request: https://github.com/git/git/pull/1041\n\nRange-diff vs v3:\n\n  1:  390a8f816cd !  1:  b4b0e2bac1c Add commit, tag & push signing via SSH keys\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    Add commit, tag & push signing via SSH keys\n     +    ssh signing: preliminary refactoring and clean-up\n      \n          Openssh v8.2p1 added some new options to ssh-keygen for signature\n          creation and verification. These allow us to use ssh keys for git\n          signatures easily.\n      \n     -    Start with adding the new signature format, new config options and\n     -    rename some fields for consistency.\n     -\n     -    This feature makes git signing much more accessible to the average user.\n     -    Usually they have a SSH Key for pushing code already. Using it\n     -    for signing commits allows us to verify not only the transport but the\n     -    pushed code as well.\n     -\n     -    In our corporate environemnt we use PIV x509 Certs on Yubikeys for email\n     -    signing/encryption and ssh keys which i think is quite common\n     +    In our corporate environment we use PIV x509 Certs on Yubikeys for email\n     +    signing/encryption and ssh keys which I think is quite common\n          (at least for the email part). This way we can establish the correct\n          trust for the SSH Keys without setting up a separate GPG Infrastructure\n          (which is still quite painful for users) or implementing x509 signing\n     @@ Commit message\n          In such a setup the keyring & revocationKeyring can be centrally\n          generated from the x509 CA information and distributed to the users.\n      \n     +    To be able to implement new signing formats this commit:\n     +     - makes the sigc structure more generic by renaming \"gpg_output\" to\n     +       \"output\"\n     +     - introduces function pointers in the gpg_format structure to call\n     +       format specific signing and verification functions\n     +     - moves format detection from verify_signed_buffer into the check_signature\n     +       api function and calls the format specific verify\n     +     - renames and wraps sign_buffer to handle format specific signing logic\n     +       as well\n     +\n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## fmt-merge-msg.c ##\n      @@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)\n     + \t\t\tbuf = payload.buf;\n       \t\t\tlen = payload.len;\n       \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n     - \t\t\t\t\t sig.len, &sigc) &&\n     +-\t\t\t\t\t sig.len, &sigc) &&\n      -\t\t\t\t!sigc.gpg_output)\n     -+\t\t\t\t!sigc.output)\n     ++\t\t\t\t\t    sig.len, &sigc) &&\n     ++\t\t\t    !sigc.output)\n       \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n       \t\t\telse\n      -\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n     @@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)\n       \n      \n       ## gpg-interface.c ##\n     -@@\n     - #include \"tempfile.h\"\n     - \n     - static char *configured_signing_key;\n     -+const char *ssh_allowed_signers, *ssh_revocation_file;\n     - static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n     +@@ gpg-interface.c: struct gpg_format {\n     + \tconst char *program;\n     + \tconst char **verify_args;\n     + \tconst char **sigs;\n     ++\tint (*verify_signed_buffer)(struct signature_check *sigc,\n     ++\t\t\t\t    struct gpg_format *fmt, const char *payload,\n     ++\t\t\t\t    size_t payload_size, const char *signature,\n     ++\t\t\t\t    size_t signature_size);\n     ++\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n     ++\t\t\t   const char *signing_key);\n     + };\n       \n     - struct gpg_format {\n     + static const char *openpgp_verify_args[] = {\n      @@ gpg-interface.c: static const char *x509_sigs[] = {\n       \tNULL\n       };\n       \n     -+static const char *ssh_verify_args[] = {\n     -+\tNULL\n     -+};\n     -+static const char *ssh_sigs[] = {\n     -+\t\"-----BEGIN SSH SIGNATURE-----\",\n     -+\tNULL\n     -+};\n     ++static int verify_gpg_signed_buffer(struct signature_check *sigc,\n     ++\t\t\t\t    struct gpg_format *fmt, const char *payload,\n     ++\t\t\t\t    size_t payload_size, const char *signature,\n     ++\t\t\t\t    size_t signature_size);\n     ++static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     ++\t\t\t   const char *signing_key);\n      +\n       static struct gpg_format gpg_format[] = {\n     - \t{ .name = \"openpgp\", .program = \"gpg\",\n     - \t  .verify_args = openpgp_verify_args,\n     -@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     - \t  .verify_args = x509_verify_args,\n     - \t  .sigs = x509_sigs\n     +-\t{ .name = \"openpgp\", .program = \"gpg\",\n     +-\t  .verify_args = openpgp_verify_args,\n     +-\t  .sigs = openpgp_sigs\n     ++\t{\n     ++\t\t.name = \"openpgp\",\n     ++\t\t.program = \"gpg\",\n     ++\t\t.verify_args = openpgp_verify_args,\n     ++\t\t.sigs = openpgp_sigs,\n     ++\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n     ++\t\t.sign_buffer = sign_buffer_gpg,\n     + \t},\n     +-\t{ .name = \"x509\", .program = \"gpgsm\",\n     +-\t  .verify_args = x509_verify_args,\n     +-\t  .sigs = x509_sigs\n     ++\t{\n     ++\t\t.name = \"x509\",\n     ++\t\t.program = \"gpgsm\",\n     ++\t\t.verify_args = x509_verify_args,\n     ++\t\t.sigs = x509_sigs,\n     ++\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n     ++\t\t.sign_buffer = sign_buffer_gpg,\n       \t},\n     -+\t{ .name = \"ssh\", .program = \"ssh-keygen\",\n     -+\t  .verify_args = ssh_verify_args,\n     -+\t  .sigs = ssh_sigs },\n       };\n       \n     - static struct gpg_format *use_format = &gpg_format[0];\n      @@ gpg-interface.c: static struct gpg_format *get_format_by_sig(const char *sig)\n       void signature_check_clear(struct signature_check *sigc)\n       {\n     @@ gpg-interface.c: error:\n      -\t\t\t\tconst char *signature, size_t signature_size,\n      -\t\t\t\tstruct strbuf *gpg_output,\n      -\t\t\t\tstruct strbuf *gpg_status)\n     -+static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,\n     -+\tconst char *payload, size_t payload_size,\n     -+\tconst char *signature, size_t signature_size)\n     ++static int verify_gpg_signed_buffer(struct signature_check *sigc,\n     ++\t\t\t\t    struct gpg_format *fmt, const char *payload,\n     ++\t\t\t\t    size_t payload_size, const char *signature,\n     ++\t\t\t\t    size_t signature_size)\n       {\n       \tstruct child_process gpg = CHILD_PROCESS_INIT;\n      -\tstruct gpg_format *fmt;\n       \tstruct tempfile *temp;\n       \tint ret;\n      -\tstruct strbuf buf = STRBUF_INIT;\n     -+\tstruct strbuf gpg_out = STRBUF_INIT;\n     -+\tstruct strbuf gpg_err = STRBUF_INIT;\n     ++\tstruct strbuf gpg_stdout = STRBUF_INIT;\n     ++\tstruct strbuf gpg_stderr = STRBUF_INIT;\n       \n       \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n       \tif (!temp)\n     @@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay\n       \tstrvec_push(&gpg.args, fmt->program);\n       \tstrvec_pushv(&gpg.args, fmt->verify_args);\n       \tstrvec_pushl(&gpg.args,\n     --\t\t     \"--status-fd=1\",\n     --\t\t     \"--verify\", temp->filename.buf, \"-\",\n     --\t\t     NULL);\n     --\n     +@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,\n     + \t\t     \"--verify\", temp->filename.buf, \"-\",\n     + \t\t     NULL);\n     + \n      -\tif (!gpg_status)\n      -\t\tgpg_status = &buf;\n     -+\t\t\t\"--status-fd=1\",\n     -+\t\t\t\"--verify\", temp->filename.buf, \"-\",\n     -+\t\t\tNULL);\n     - \n     +-\n       \tsigchain_push(SIGPIPE, SIG_IGN);\n      -\tret = pipe_command(&gpg, payload, payload_size,\n      -\t\t\t   gpg_status, 0, gpg_output, 0);\n     -+\tret = pipe_command(&gpg, payload, payload_size, &gpg_out, 0,\n     -+\t\t\t\t&gpg_err, 0);\n     ++\tret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,\n     ++\t\t\t   &gpg_stderr, 0);\n       \tsigchain_pop(SIGPIPE);\n     -+\tret |= !strstr(gpg_out.buf, \"\\n[GNUPG:] GOODSIG \");\n       \n     --\tdelete_tempfile(&temp);\n     -+\tsigc->payload = xmemdupz(payload, payload_size);\n     -+\tsigc->output = strbuf_detach(&gpg_err, NULL);\n     -+\tsigc->gpg_status = strbuf_detach(&gpg_out, NULL);\n     + \tdelete_tempfile(&temp);\n       \n      -\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n      -\tstrbuf_release(&buf); /* no matter it was used or not */\n     ++\tret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n     ++\tsigc->payload = xmemdupz(payload, payload_size);\n     ++\tsigc->output = strbuf_detach(&gpg_stderr, NULL);\n     ++\tsigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);\n     ++\n      +\tparse_gpg_output(sigc);\n      +\n     -+\tdelete_tempfile(&temp);\n     -+\tstrbuf_release(&gpg_out);\n     -+\tstrbuf_release(&gpg_err);\n     ++\tstrbuf_release(&gpg_stdout);\n     ++\tstrbuf_release(&gpg_stderr);\n       \n       \treturn ret;\n       }\n     @@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay\n      -\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n      -\tparse_gpg_output(sigc);\n      +\tfmt = get_format_by_sig(signature);\n     -+\tif (!fmt) {\n     -+\t\terror(_(\"bad/incompatible signature '%s'\"), signature);\n     -+\t\treturn -1;\n     -+\t}\n     ++\tif (!fmt)\n     ++\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n     ++\n     ++\tstatus = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,\n     ++\t\t\t\t\t   slen);\n      +\n     -+\tif (!strcmp(fmt->name, \"ssh\")) {\n     -+\t\tstatus = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);\n     -+\t} else {\n     -+\t\tstatus = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);\n     -+\t}\n      +\tif (status && !sigc->output)\n      +\t\treturn !!status;\n      +\n     @@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay\n       \n       void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n       {\n     - \tconst char *output = flags & GPG_VERIFY_RAW ?\n     +-\tconst char *output = flags & GPG_VERIFY_RAW ?\n      -\t\tsigc->gpg_status : sigc->gpg_output;\n     -+\t\tsigc->gpg_status : sigc->output;\n     ++\tconst char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :\n     ++\t\t\t\t\t\t\t    sigc->output;\n       \n       \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n       \t\tfputs(sigc->payload, stdout);\n     -@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     - \tint ret;\n     - \n     - \tif (!strcmp(var, \"user.signingkey\")) {\n     -+\t\t/*\n     -+\t\t * user.signingkey can contain one of the following\n     -+\t\t * when format = openpgp/x509\n     -+\t\t *   - GPG KeyID\n     -+\t\t * when format = ssh\n     -+\t\t *   - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)\n     -+\t\t *   - path to a file containing a public or a private ssh key\n     -+\t\t */\n     - \t\tif (!value)\n     - \t\t\treturn config_error_nonbool(var);\n     - \t\tset_signing_key(value);\n     - \t\treturn 0;\n     - \t}\n     - \n     -+\tif (!strcmp(var, \"gpg.ssh.keyring\")) {\n     -+\t\tif (!value)\n     -+\t\t\treturn config_error_nonbool(var);\n     -+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n     -+\t}\n     -+\n     -+\tif (!strcmp(var, \"gpg.ssh.revocationkeyring\")) {\n     -+\t\tif (!value)\n     -+\t\t\treturn config_error_nonbool(var);\n     -+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n     -+\t}\n     -+\n     - \tif (!strcmp(var, \"gpg.format\")) {\n     - \t\tif (!value)\n     - \t\t\treturn config_error_nonbool(var);\n     -@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     - \tif (!strcmp(var, \"gpg.x509.program\"))\n     - \t\tfmtname = \"x509\";\n     - \n     -+\tif (!strcmp(var, \"gpg.ssh.program\"))\n     -+\t\tfmtname = \"ssh\";\n     -+\n     - \tif (fmtname) {\n     - \t\tfmt = get_format_by_name(fmtname);\n     - \t\treturn git_config_string(&fmt->program, var, value);\n      @@ gpg-interface.c: const char *get_signing_key(void)\n     - {\n     - \tif (configured_signing_key)\n     - \t\treturn configured_signing_key;\n     --\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\treturn get_default_ssh_signing_key();\n     -+\t} else {\n     -+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n     -+\t}\n     -+}\n     -+\n     -+const char *get_ssh_allowed_signers(void)\n     -+{\n     -+\tif (ssh_allowed_signers)\n     -+\t\treturn ssh_allowed_signers;\n     -+\n     -+\tdie(\"A Path to an allowed signers ssh keyring is needed for validation\");\n       }\n       \n       int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n     ++{\n     ++\treturn use_format->sign_buffer(buffer, signature, signing_key);\n     ++}\n     ++\n     ++static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     ++\t\t    const char *signing_key)\n     + {\n     + \tstruct child_process gpg = CHILD_PROCESS_INIT;\n     + \tint ret;\n      \n       ## gpg-interface.h ##\n      @@ gpg-interface.h: enum signature_trust_level {\n     @@ gpg-interface.h: enum signature_trust_level {\n       struct signature_check {\n       \tchar *payload;\n      -\tchar *gpg_output;\n     --\tchar *gpg_status;\n      +\tchar *output;\n     -+\tchar *gpg_status; /* Only used internally -> remove from this public api */\n     + \tchar *gpg_status;\n       \n       \t/*\n     - \t * possible \"result\":\n     -@@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n     - int git_gpg_config(const char *, const char *, void *);\n     - void set_signing_key(const char *);\n     - const char *get_signing_key(void);\n     -+const char *get_ssh_allowed_signers(void);\n     - int check_signature(const char *payload, size_t plen,\n     - \t\t    const char *signature, size_t slen,\n     - \t\t    struct signature_check *sigc);\n      \n       ## log-tree.c ##\n      @@ log-tree.c: static void show_signature(struct rev_info *opt, struct commit *commit)\n  4:  df55b9e1d59 !  2:  2c75adee8e1 ssh signing: sign using either gpg or ssh keys\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    ssh signing: sign using either gpg or ssh keys\n     +    ssh signing: add ssh signature format and signing using ssh keys\n      \n     -    implements the actual ssh-keygen -Y sign operation\n     +    implements the actual sign_buffer_ssh operation and move some shared\n     +    cleanup code into a strbuf function\n      \n          Set gpg.format = ssh and user.signingkey to either a ssh public key\n          string (like from an authorized_keys file), or a ssh key file.\n          If the key file or the config value itself contains only a public key\n          then the private key needs to be available via ssh-agent.\n     -    If no signingkey is set then git will call 'ssh-add -L' to check for\n     -    available agent keys and use the first one for signing.\n     +\n     +    gpg.ssh.program can be set to an alternative location of ssh-keygen.\n     +    A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\n     +    this feature. Since only ssh-keygen is needed it can this way be\n     +    installed seperately without upgrading your system openssh packages.\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## gpg-interface.c ##\n     -@@ gpg-interface.c: const char *get_ssh_allowed_signers(void)\n     +@@ gpg-interface.c: static const char *x509_sigs[] = {\n     + \tNULL\n     + };\n     + \n     ++static const char *ssh_verify_args[] = { NULL };\n     ++static const char *ssh_sigs[] = {\n     ++\t\"-----BEGIN SSH SIGNATURE-----\",\n     ++\tNULL\n     ++};\n     ++\n     + static int verify_gpg_signed_buffer(struct signature_check *sigc,\n     + \t\t\t\t    struct gpg_format *fmt, const char *payload,\n     + \t\t\t\t    size_t payload_size, const char *signature,\n     + \t\t\t\t    size_t signature_size);\n     + static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     + \t\t\t   const char *signing_key);\n     ++static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n     ++\t\t\t   const char *signing_key);\n     + \n     + static struct gpg_format gpg_format[] = {\n     + \t{\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n     + \t\t.sign_buffer = sign_buffer_gpg,\n     + \t},\n     ++\t{\n     ++\t\t.name = \"ssh\",\n     ++\t\t.program = \"ssh-keygen\",\n     ++\t\t.verify_args = ssh_verify_args,\n     ++\t\t.sigs = ssh_sigs,\n     ++\t\t.verify_signed_buffer = NULL, /* TODO */\n     ++\t\t.sign_buffer = sign_buffer_ssh\n     ++\t},\n     + };\n     + \n     + static struct gpg_format *use_format = &gpg_format[0];\n     +@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     + \tif (!strcmp(var, \"gpg.x509.program\"))\n     + \t\tfmtname = \"x509\";\n     + \n     ++\tif (!strcmp(var, \"gpg.ssh.program\"))\n     ++\t\tfmtname = \"ssh\";\n     ++\n     + \tif (fmtname) {\n     + \t\tfmt = get_format_by_name(fmtname);\n     + \t\treturn git_config_string(&fmt->program, var, value);\n     +@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n     + \treturn use_format->sign_buffer(buffer, signature, signing_key);\n     + }\n       \n     - int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n     ++static void strbuf_trim_trailing_cr(struct strbuf *buffer, int offset)\n     ++{\n     ++\tsize_t i, j;\n     ++\n     ++\tfor (i = j = offset; i < buffer->len; i++) {\n     ++\t\tif (buffer->buf[i] != '\\r') {\n     ++\t\t\tif (i != j)\n     ++\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n     ++\t\t\tj++;\n     ++\t\t}\n     ++\t}\n     ++\tstrbuf_setlen(buffer, j);\n     ++}\n     ++\n     + static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     + \t\t    const char *signing_key)\n       {\n     --\tstruct child_process gpg = CHILD_PROCESS_INIT;\n     -+\tstruct child_process signer = CHILD_PROCESS_INIT;\n     + \tstruct child_process gpg = CHILD_PROCESS_INIT;\n       \tint ret;\n     - \tsize_t i, j, bottom;\n     --\tstruct strbuf gpg_status = STRBUF_INIT;\n     +-\tsize_t i, j, bottom;\n     ++\tsize_t bottom;\n     + \tstruct strbuf gpg_status = STRBUF_INIT;\n     + \n     + \tstrvec_pushl(&gpg.args,\n     +@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     + \t\treturn error(_(\"gpg failed to sign the data\"));\n     + \n     + \t/* Strip CR from the line endings, in case we are on Windows. */\n     +-\tfor (i = j = bottom; i < signature->len; i++)\n     +-\t\tif (signature->buf[i] != '\\r') {\n     +-\t\t\tif (i != j)\n     +-\t\t\t\tsignature->buf[j] = signature->buf[i];\n     +-\t\t\tj++;\n     +-\t\t}\n     +-\tstrbuf_setlen(signature, j);\n     ++\tstrbuf_trim_trailing_cr(signature, bottom);\n     + \n     + \treturn 0;\n     + }\n     ++\n     ++static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n     ++\t\t\t   const char *signing_key)\n     ++{\n     ++\tstruct child_process signer = CHILD_PROCESS_INIT;\n     ++\tint ret = -1;\n     ++\tsize_t bottom;\n      +\tstruct strbuf signer_stderr = STRBUF_INIT;\n      +\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n      +\tchar *ssh_signing_key_file = NULL;\n      +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n     - \n     --\tstrvec_pushl(&gpg.args,\n     --\t\t     use_format->program,\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\tif (!signing_key || signing_key[0] == '\\0')\n     -+\t\t\treturn error(_(\"user.signingkey needs to be set for ssh signing\"));\n     -+\n     -+\n     -+\t\tif (istarts_with(signing_key, \"ssh-\")) {\n     -+\t\t\t/* A literal ssh key */\n     -+\t\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n     -+\t\t\tif (!temp)\n     -+\t\t\t\treturn error_errno(_(\"could not create temporary file\"));\n     -+\t\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) < 0 ||\n     -+\t\t\t\tclose_tempfile_gently(temp) < 0) {\n     -+\t\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n     -+\t\t\t\t\ttemp->filename.buf);\n     -+\t\t\t\tdelete_tempfile(&temp);\n     -+\t\t\t\treturn -1;\n     -+\t\t\t}\n     -+\t\t\tssh_signing_key_file= temp->filename.buf;\n     -+\t\t} else {\n     -+\t\t\t/* We assume a file */\n     -+\t\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n     -+\t\t}\n      +\n     -+\t\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n     -+\t\tif (!buffer_file)\n     -+\t\t\treturn error_errno(_(\"could not create temporary file\"));\n     -+\t\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n     -+\t\t\tclose_tempfile_gently(buffer_file) < 0) {\n     -+\t\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n     -+\t\t\t\tbuffer_file->filename.buf);\n     -+\t\t\tdelete_tempfile(&buffer_file);\n     -+\t\t\treturn -1;\n     -+\t\t}\n     ++\tif (!signing_key || signing_key[0] == '\\0')\n     ++\t\treturn error(\n     ++\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n      +\n     -+\t\tstrvec_pushl(&signer.args, use_format->program ,\n     -+\t\t\t\t\t\"-Y\", \"sign\",\n     -+\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\"-f\", ssh_signing_key_file,\n     -+\t\t\t\t\tbuffer_file->filename.buf,\n     -+\t\t\t\t\tNULL);\n     -+\n     -+\t\tsigchain_push(SIGPIPE, SIG_IGN);\n     -+\t\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n     -+\t\tsigchain_pop(SIGPIPE);\n     -+\n     -+\t\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n     -+\t\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n     -+\t\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n     -+\t\t\terror_errno(_(\"failed reading ssh signing data buffer from '%s'\"),\n     -+\t\t\t\tssh_signature_filename.buf);\n     ++\tif (istarts_with(signing_key, \"ssh-\")) {\n     ++\t\t/* A literal ssh key */\n     ++\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n     ++\t\tif (!temp)\n     ++\t\t\treturn error_errno(\n     ++\t\t\t\t_(\"could not create temporary file\"));\n     ++\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) <\n     ++\t\t\t    0 ||\n     ++\t\t    close_tempfile_gently(temp) < 0) {\n     ++\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n     ++\t\t\t\t    temp->filename.buf);\n     ++\t\t\tgoto out;\n      +\t\t}\n     -+\t\tunlink_or_warn(ssh_signature_filename.buf);\n     -+\t\tstrbuf_release(&ssh_signature_filename);\n     -+\t\tdelete_tempfile(&buffer_file);\n     ++\t\tssh_signing_key_file = temp->filename.buf;\n      +\t} else {\n     -+\t\tstrvec_pushl(&signer.args, use_format->program ,\n     - \t\t     \"--status-fd=2\",\n     - \t\t     \"-bsau\", signing_key,\n     - \t\t     NULL);\n     - \n     --\tbottom = signature->len;\n     --\n     - \t/*\n     - \t * When the username signingkey is bad, program could be terminated\n     - \t * because gpg exits without reading and then write gets SIGPIPE.\n     - \t */\n     - \tsigchain_push(SIGPIPE, SIG_IGN);\n     --\tret = pipe_command(&gpg, buffer->buf, buffer->len,\n     --\t\t\t   signature, 1024, &gpg_status, 0);\n     -+\t\tret = pipe_command(&signer, buffer->buf, buffer->len, signature, 1024, &signer_stderr, 0);\n     - \tsigchain_pop(SIGPIPE);\n     ++\t\t/* We assume a file */\n     ++\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n     ++\t}\n     ++\n     ++\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n     ++\tif (!buffer_file) {\n     ++\t\terror_errno(_(\"could not create temporary file\"));\n     ++\t\tgoto out;\n     ++\t}\n     ++\n     ++\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n     ++\t    close_tempfile_gently(buffer_file) < 0) {\n     ++\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n     ++\t\t\t    buffer_file->filename.buf);\n     ++\t\tgoto out;\n     ++\t}\n     ++\n     ++\tstrvec_pushl(&signer.args, use_format->program, \"-Y\", \"sign\", \"-n\",\n     ++\t\t     \"git\", \"-f\", ssh_signing_key_file,\n     ++\t\t     buffer_file->filename.buf, NULL);\n     ++\n     ++\tsigchain_push(SIGPIPE, SIG_IGN);\n     ++\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n     ++\tsigchain_pop(SIGPIPE);\n     ++\n     ++\tif (ret && strstr(signer_stderr.buf, \"usage:\")) {\n     ++\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n     ++\t\tgoto out;\n     ++\t}\n     ++\n     ++\tif (ret) {\n     ++\t\terror(\"%s\", signer_stderr.buf);\n     ++\t\tgoto out;\n      +\t}\n      +\n      +\tbottom = signature->len;\n      +\n     ++\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n     ++\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n     ++\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n     ++\t\terror_errno(\n     ++\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n     ++\t\t\tssh_signature_filename.buf);\n     ++\t}\n     ++\tunlink_or_warn(ssh_signature_filename.buf);\n     ++\n     ++\tif (ret) {\n     ++\t\terror(_(\"ssh failed to sign the data\"));\n     ++\t\tgoto out;\n     ++\t}\n     ++\n     ++\t/* Strip CR from the line endings, in case we are on Windows. */\n     ++\tstrbuf_trim_trailing_cr(signature, bottom);\n     ++\n     ++out:\n      +\tif (temp)\n      +\t\tdelete_tempfile(&temp);\n     - \n     --\tret |= !strstr(gpg_status.buf, \"\\n[GNUPG:] SIG_CREATED \");\n     --\tstrbuf_release(&gpg_status);\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\tif (strstr(signer_stderr.buf, \"usage:\")) {\n     -+\t\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)\"));\n     -+\t\t}\n     -+\t} else {\n     -+\t\tret |= !strstr(signer_stderr.buf, \"\\n[GNUPG:] SIG_CREATED \");\n     -+\t}\n     ++\tif (buffer_file)\n     ++\t\tdelete_tempfile(&buffer_file);\n      +\tstrbuf_release(&signer_stderr);\n     - \tif (ret)\n     - \t\treturn error(_(\"gpg failed to sign the data\"));\n     - \n     ++\tstrbuf_release(&ssh_signature_filename);\n     ++\treturn ret;\n     ++}\n  3:  b84b2812470 !  3:  1ec5c06cbe9 ssh signing: retrieve a default key from ssh-agent\n     @@ Metadata\n       ## Commit message ##\n          ssh signing: retrieve a default key from ssh-agent\n      \n     -    calls ssh-add -L and uses the first key\n     +    if user.signingkey is not set and a ssh signature is requested we call\n     +    ssh-add -L and use the first key we get\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n       }\n       \n      +/* Returns the first public key from an ssh-agent to use for signing */\n     -+static char *get_default_ssh_signing_key(void) {\n     ++static char *get_default_ssh_signing_key(void)\n     ++{\n      +\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n      +\tint ret = -1;\n      +\tstruct strbuf key_stdout = STRBUF_INIT;\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n      +\t\t\treturn strbuf_detach(keys[0], NULL);\n      +\t}\n      +\n     ++\tstrbuf_release(&key_stdout);\n      +\treturn \"\";\n      +}\n     ++\n       const char *get_signing_key(void)\n       {\n       \tif (configured_signing_key)\n     + \t\treturn configured_signing_key;\n     +-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n     ++\tif (!strcmp(use_format->name, \"ssh\")) {\n     ++\t\treturn get_default_ssh_signing_key();\n     ++\t} else {\n     ++\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n     ++\t}\n     + }\n     + \n     + int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n  5:  0581c72634c !  4:  ec6931082ee ssh signing: provide a textual representation of the signing key\n     @@ Metadata\n       ## Commit message ##\n          ssh signing: provide a textual representation of the signing key\n      \n     -    for ssh the key can be a filename/path or even a literal ssh pubkey\n     -    in push certs and textual output we prefer the ssh fingerprint instead\n     +    for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\n     +    in push certs and textual output we prefer the ssh fingerprint instead.\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n       \treturn 0;\n       }\n       \n     -+static char *get_ssh_key_fingerprint(const char *signing_key) {\n     ++static char *get_ssh_key_fingerprint(const char *signing_key)\n     ++{\n      +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n      +\tint ret = -1;\n      +\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n      +\t * For textual representation we usually want a fingerprint\n      +\t */\n      +\tif (istarts_with(signing_key, \"ssh-\")) {\n     -+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n     -+\t\t\t\t\t\"-lf\", \"-\",\n     -+\t\t\t\t\tNULL);\n     -+\t\tret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key),\n     -+\t\t\t&fingerprint_stdout, 0,  NULL, 0);\n     ++\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n     ++\t\tret = pipe_command(&ssh_keygen, signing_key,\n     ++\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n     ++\t\t\t\t   NULL, 0);\n      +\t} else {\n     -+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\",\n     -+\t\t\t\t\t\"-lf\", configured_signing_key,\n     -+\t\t\t\t\tNULL);\n     ++\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\",\n     ++\t\t\t     configured_signing_key, NULL);\n      +\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n     -+\t\t\tNULL, 0);\n     ++\t\t\t\t   NULL, 0);\n      +\t}\n      +\n      +\tif (!!ret)\n      +\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n     -+\t\t\tsigning_key);\n     ++\t\t\t  signing_key);\n      +\n      +\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n      +\tif (!fingerprint[1])\n      +\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n     -+\t\t\tsigning_key);\n     ++\t\t\t  signing_key);\n      +\n      +\treturn strbuf_detach(fingerprint[1], NULL);\n      +}\n      +\n       /* Returns the first public key from an ssh-agent to use for signing */\n     - static char *get_default_ssh_signing_key(void) {\n     - \tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n     -@@ gpg-interface.c: static char *get_default_ssh_signing_key(void) {\n     - \n     + static char *get_default_ssh_signing_key(void)\n     + {\n     +@@ gpg-interface.c: static char *get_default_ssh_signing_key(void)\n       \treturn \"\";\n       }\n     -+\n     + \n      +/* Returns a textual but unique representation ot the signing key */\n     -+const char *get_signing_key_id(void) {\n     ++const char *get_signing_key_id(void)\n     ++{\n      +\tif (!strcmp(use_format->name, \"ssh\")) {\n      +\t\treturn get_ssh_key_fingerprint(get_signing_key());\n      +\t} else {\n     @@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature\n      + * Either a GPG KeyID or a SSH Key Fingerprint\n      + */\n      +const char *get_signing_key_id(void);\n     -+\n     - const char *get_ssh_allowed_signers(void);\n       int check_signature(const char *payload, size_t plen,\n       \t\t    const char *signature, size_t slen,\n     + \t\t    struct signature_check *sigc);\n      \n       ## send-pack.c ##\n      @@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,\n  6:  381a950a6e1 !  5:  4436cb3a122 ssh signing: parse ssh-keygen output and verify signatures\n     @@ Metadata\n       ## Commit message ##\n          ssh signing: parse ssh-keygen output and verify signatures\n      \n     -    Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) \"ALLOWED\n     +    to verify a ssh signature we first call ssh-keygen -Y find-principal to\n     +    look up the signing principal by their public key from the\n     +    allowedSignersFile. If the key is found then we do a verify. Otherwise\n     +    we only validate the signature but can not verify the signers identity.\n     +\n     +    Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\n          SIGNERS\") which contains valid public keys and a principal (usually\n          user@domain). Depending on the environment this file can be managed by\n          the individual developer or for example generated by the central\n     @@ Commit message\n          To revoke a key put the public key without the principal prefix into\n          gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n          \"KEY REVOCATION LISTS\"). The same considerations about who to trust for\n     -    verification as with the keyring file apply.\n     +    verification as with the allowedSignersFile apply.\n      \n          Using SSH CA Keys with these files is also possible. Add\n          \"cert-authority\" as key option between the principal and the key to mark\n     @@ gpg-interface.c\n       #include \"gpg-interface.h\"\n       #include \"sigchain.h\"\n       #include \"tempfile.h\"\n     -@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,\n     - \treturn 1;\n     + \n     + static char *configured_signing_key;\n     ++static const char *ssh_allowed_signers, *ssh_revocation_file;\n     + static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n     + \n     + struct gpg_format {\n     +@@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sigc,\n     + \t\t\t\t    struct gpg_format *fmt, const char *payload,\n     + \t\t\t\t    size_t payload_size, const char *signature,\n     + \t\t\t\t    size_t signature_size);\n     ++static int verify_ssh_signed_buffer(struct signature_check *sigc,\n     ++\t\t\t\t    struct gpg_format *fmt, const char *payload,\n     ++\t\t\t\t    size_t payload_size, const char *signature,\n     ++\t\t\t\t    size_t signature_size);\n     + static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     + \t\t\t   const char *signing_key);\n     + static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.program = \"ssh-keygen\",\n     + \t\t.verify_args = ssh_verify_args,\n     + \t\t.sigs = ssh_sigs,\n     +-\t\t.verify_signed_buffer = NULL, /* TODO */\n     ++\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n     + \t\t.sign_buffer = sign_buffer_ssh\n     + \t},\n     + };\n     +@@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sigc,\n     + \treturn ret;\n       }\n       \n      +static void parse_ssh_output(struct signature_check *sigc)\n      +{\n     -+\tstruct string_list parts = STRING_LIST_INIT_DUP;\n     -+\tchar *line = NULL;\n     ++\tconst char *line, *principal, *search;\n      +\n      +\t/*\n      +\t * ssh-keysign output should be:\n      +\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n     ++\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n      +\t * or for valid but unknown keys:\n      +\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n      +\t */\n     @@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,\n      +\tsigc->trust_level = TRUST_NEVER;\n      +\n      +\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n     -+\tstring_list_split(&parts, line, ' ', 8);\n     -+\tif (parts.nr >= 9 && starts_with(line, \"Good \\\"git\\\" signature for \")) {\n     -+\t\t/* Valid signature for a trusted signer */\n     ++\n     ++\tif (skip_prefix(line, \"Good \\\"git\\\" signature for \", &line)) {\n     ++\t\t/* Valid signature and known principal */\n      +\t\tsigc->result = 'G';\n      +\t\tsigc->trust_level = TRUST_FULLY;\n     -+\t\tsigc->signer = xstrdup(parts.items[4].string);\n     -+\t\tsigc->fingerprint = xstrdup(parts.items[8].string);\n     ++\n     ++\t\t/* Search for the last \"with\" to get the full principal */\n     ++\t\tprincipal = line;\n     ++\t\tdo {\n     ++\t\t\tsearch = strstr(line, \" with \");\n     ++\t\t\tif (search)\n     ++\t\t\t\tline = search + 1;\n     ++\t\t} while (search != NULL);\n     ++\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n     ++\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n      +\t\tsigc->key = xstrdup(sigc->fingerprint);\n     -+\t} else if (parts.nr >= 7 && starts_with(line, \"Good \\\"git\\\" signature with \")) {\n     ++\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n      +\t\t/* Valid signature, but key unknown */\n      +\t\tsigc->result = 'G';\n      +\t\tsigc->trust_level = TRUST_UNDEFINED;\n     -+\t\tsigc->fingerprint = xstrdup(parts.items[6].string);\n     ++\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n      +\t\tsigc->key = xstrdup(sigc->fingerprint);\n      +\t}\n     -+\ttrace_printf(\"trace: sigc result %c/%d - %s %s %s\", sigc->result, sigc->trust_level, sigc->signer, sigc->fingerprint, sigc->key);\n     ++}\n     ++\n     ++static const char *get_ssh_allowed_signers(void)\n     ++{\n     ++\tif (ssh_allowed_signers)\n     ++\t\treturn ssh_allowed_signers;\n      +\n     -+\tstring_list_clear(&parts, 0);\n     -+\tFREE_AND_NULL(line);\n     ++\tdie(\"gpg.ssh.allowedSignersFile needs to be configured and exist for validation\");\n      +}\n      +\n     - static void parse_gpg_output(struct signature_check *sigc)\n     - {\n     - \tconst char *buf = sigc->gpg_status;\n     -@@ gpg-interface.c: error:\n     - \tFREE_AND_NULL(sigc->key);\n     - }\n     - \n     -+static int verify_ssh_signature(struct signature_check *sigc,\n     -+\tstruct gpg_format *fmt,\n     -+\tconst char *payload, size_t payload_size,\n     -+\tconst char *signature, size_t signature_size)\n     ++static int verify_ssh_signed_buffer(struct signature_check *sigc,\n     ++\t\t\t\t    struct gpg_format *fmt, const char *payload,\n     ++\t\t\t\t    size_t payload_size, const char *signature,\n     ++\t\t\t\t    size_t signature_size)\n      +{\n      +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n      +\tstruct tempfile *temp;\n     @@ gpg-interface.c: error:\n      +\t}\n      +\n      +\t/* Find the principal from the signers */\n     -+\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n     -+\t\t\t\t\t\"-Y\", \"find-principals\",\n     -+\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     -+\t\t\t\t\t\"-s\", temp->filename.buf,\n     -+\t\t\t\t\tNULL);\n     -+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     -+\tif (strstr(ssh_keygen_err.buf, \"usage:\")) {\n     -+\t\terror(_(\"openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)\"));\n     ++\tstrvec_pushl(&ssh_keygen.args, fmt->program, \"-Y\", \"find-principals\",\n     ++\t\t     \"-f\", get_ssh_allowed_signers(), \"-s\", temp->filename.buf,\n     ++\t\t     NULL);\n     ++\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n     ++\t\t\t   &ssh_keygen_err, 0);\n     ++\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n     ++\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n     ++\t\treturn ret;\n      +\t}\n      +\tif (ret || !ssh_keygen_out.len) {\n     -+\t\t/* We did not find a matching principal in the keyring - Check without validation */\n     ++\t\t/* We did not find a matching principal in the allowedSigners - Check\n     ++\t\t * without validation */\n      +\t\tchild_process_init(&ssh_keygen);\n     -+\t\tstrvec_pushl(&ssh_keygen.args,  fmt->program,\n     -+\t\t\t\t\t\t\"-Y\", \"check-novalidate\",\n     -+\t\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\t\"-s\", temp->filename.buf,\n     -+\t\t\t\t\t\tNULL);\n     -+\t\tret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     ++\t\tstrvec_pushl(&ssh_keygen.args, fmt->program, \"-Y\",\n     ++\t\t\t     \"check-novalidate\", \"-n\", \"git\", \"-s\",\n     ++\t\t\t     temp->filename.buf, NULL);\n     ++\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n     ++\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n      +\t} else {\n      +\t\t/* Check every principal we found (one per line) */\n     -+\t\tfor (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\\n')) {\n     ++\t\tfor (line = ssh_keygen_out.buf; *line;\n     ++\t\t     line = strchrnul(line + 1, '\\n')) {\n      +\t\t\twhile (*line == '\\n')\n      +\t\t\t\tline++;\n      +\t\t\tif (!*line)\n      +\t\t\t\tbreak;\n      +\n     -+\t\t\ttrust_size = strcspn(line, \" \\n\");\n     ++\t\t\ttrust_size = strcspn(line, \"\\n\");\n      +\t\t\tprincipal = xmemdupz(line, trust_size);\n      +\n      +\t\t\tchild_process_init(&ssh_keygen);\n      +\t\t\tstrbuf_release(&ssh_keygen_out);\n      +\t\t\tstrbuf_release(&ssh_keygen_err);\n     -+\t\t\tstrvec_push(&ssh_keygen.args,fmt->program);\n     -+\t\t\t/* We found principals - Try with each until we find a match */\n     -+\t\t\tstrvec_pushl(&ssh_keygen.args,  \"-Y\", \"verify\",\n     -+\t\t\t\t\t\t\t\"-n\", \"git\",\n     -+\t\t\t\t\t\t\t\"-f\", get_ssh_allowed_signers(),\n     -+\t\t\t\t\t\t\t\"-I\", principal,\n     -+\t\t\t\t\t\t\t\"-s\", temp->filename.buf,\n     -+\t\t\t\t\t\t\tNULL);\n     ++\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n     ++\t\t\t/* We found principals - Try with each until we find a\n     ++\t\t\t * match */\n     ++\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\", \"-n\",\n     ++\t\t\t\t     \"git\", \"-f\", get_ssh_allowed_signers(),\n     ++\t\t\t\t     \"-I\", principal, \"-s\", temp->filename.buf,\n     ++\t\t\t\t     NULL);\n      +\n      +\t\t\tif (ssh_revocation_file) {\n      +\t\t\t\tif (file_exists(ssh_revocation_file)) {\n     -+\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\", ssh_revocation_file, NULL);\n     ++\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\",\n     ++\t\t\t\t\t\t     ssh_revocation_file, NULL);\n      +\t\t\t\t} else {\n     -+\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"), ssh_revocation_file);\n     ++\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"),\n     ++\t\t\t\t\t\tssh_revocation_file);\n      +\t\t\t\t}\n      +\t\t\t}\n      +\n      +\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n      +\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n     -+\t\t\t\t\t&ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     ++\t\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n      +\t\t\tsigchain_pop(SIGPIPE);\n      +\n     ++\t\t\tFREE_AND_NULL(principal);\n     ++\n      +\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n      +\t\t\tif (ret == 0)\n      +\t\t\t\tbreak;\n     @@ gpg-interface.c: error:\n      +\treturn ret;\n      +}\n      +\n     - static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,\n     - \tconst char *payload, size_t payload_size,\n     - \tconst char *signature, size_t signature_size)\n     + int check_signature(const char *payload, size_t plen, const char *signature,\n     + \tsize_t slen, struct signature_check *sigc)\n     + {\n     +@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     + \t\treturn 0;\n     + \t}\n     + \n     ++\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n     ++\t\tif (!value)\n     ++\t\t\treturn config_error_nonbool(var);\n     ++\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n     ++\t}\n     ++\n     ++\tif (!strcmp(var, \"gpg.ssh.revocationFile\")) {\n     ++\t\tif (!value)\n     ++\t\t\treturn config_error_nonbool(var);\n     ++\t\treturn git_config_string(&ssh_revocation_file, var, value);\n     ++\t}\n     ++\n     + \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n     + \t\tfmtname = \"openpgp\";\n     + \n  7:  1d292a8d7a2 !  6:  06a76e64b35 ssh signing: add test prereqs\n     @@ Metadata\n       ## Commit message ##\n          ssh signing: add test prereqs\n      \n     -    generate some ssh keys and a allowed keys keyring for testing\n     +    generate some ssh keys and a allowedSignersFile for testing\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '\n      +\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n      +\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n      +\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n     -+\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"principal_\\\" NR \\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n     -+\tcat \"${GNUPGHOME}/ssh.all_valid.keyring\" &&\n     ++\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"\\\\\\\"principal with number \\\" NR \\\"\\\\\\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     ++\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n      +\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n      +'\n      +\n     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '\n      +SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n      +SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n      +SIGNING_KEY_PASSPHRASE=\"super_secret\"\n     -+SIGNING_KEYRING=\"${GNUPGHOME}/ssh.all_valid.keyring\"\n     ++SIGNING_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n      +\n      +GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n      +GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n  8:  338d1b976e9 !  7:  4dc5572083b ssh signing: duplicate t7510 tests for commits\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify and show signatures' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\ttest_config gpg.mintrustlevel UNDEFINED &&\n      +\t(\n      +\t\tfor commit in initial second merge fourth-signed \\\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit verify-commit eighth-signed-alt 2>actual &&\n      +\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n      +\t! grep \"${BAD_SIGNATURE}\" actual &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\ttest_config gpg.minTrustLevel fully &&\n      +\tgit verify-commit sixth-signed\n      +'\n      +\n      +test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\ttest_config gpg.minTrustLevel marginal &&\n      +\tgit verify-commit sixth-signed\n      +'\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify signatures with --raw' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\t(\n      +\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n      +\t\tdo\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show signed commit with signature' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit show -s initial >commit &&\n      +\tgit show -s --show-signature initial >show &&\n      +\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'detect fudged signature' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit cat-file commit seventh-signed >raw &&\n      +\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n      +\tgit hash-object -w -t commit forged1 >forged1.commit &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'detect fudged signature with NUL' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit cat-file commit seventh-signed >raw &&\n      +\tcat raw >forged2 &&\n      +\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +test_expect_success GPGSSH 'amending already signed commit' '\n      +\ttest_config gpg.format ssh &&\n      +\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit checkout fourth-signed^0 &&\n      +\tgit commit --amend -S --no-edit &&\n      +\tgit verify-commit HEAD &&\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show good signature with custom format' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tG\n      +\tFINGERPRINT\n     -+\tprincipal_1\n     ++\tprincipal with number 1\n      +\tFINGERPRINT\n      +\n      +\tEOF\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show bad signature with custom format' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect <<-\\EOF &&\n      +\tB\n      +\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show untrusted signature with custom format' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tU\n      +\tFINGERPRINT\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tundefined\n      +\tFINGERPRINT\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tfully\n      +\tFINGERPRINT\n     -+\tprincipal_1\n     ++\tprincipal with number 1\n      +\tFINGERPRINT\n      +\n      +\tEOF\n     @@ t/t7527-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\ttest_config log.showsignature true &&\n      +\tgit show initial >actual &&\n      +\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n  9:  33330fda441 !  8:  275dd8a1013 ssh signing: add more tests for logs, tags & push certs\n     @@ t/t4202-log.sh: test_expect_success GPGSM 'log OpenPGP fingerprint' '\n       '\n       \n      +test_expect_success GPGSSH 'log ssh key fingerprint' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n      +\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n      +\ttest_cmp expect actual\n     @@ t/t4202-log.sh: test_expect_success GPGSM 'log --graph --show-signature x509' '\n       '\n       \n      +test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n      +\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n      +'\n     @@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certific\n      +test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n      +\tprepare_dst &&\n      +\tmkdir -p dst/.git/hooks &&\n     -+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit -C dst config receive.certnonceseed sekrit &&\n      +\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n      +\t# discard the update list\n     @@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certific\n      +\n      +\t(\n      +\t\tcat <<-\\EOF &&\n     -+\t\tSIGNER=principal_1\n     ++\t\tSIGNER=principal with number 1\n      +\t\tKEY=FINGERPRINT\n      +\t\tSTATUS=G\n      +\t\tNONCE_STATUS=OK\n     @@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed use\n      +\ttest_config gpg.format ssh &&\n      +\tprepare_dst &&\n      +\tmkdir -p dst/.git/hooks &&\n     -+\tgit -C dst config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit -C dst config receive.certnonceseed sekrit &&\n      +\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n      +\t# discard the update list\n     @@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed use\n      +\n      +\t(\n      +\t\tcat <<-\\EOF &&\n     -+\t\tSIGNER=principal_1\n     ++\t\tSIGNER=principal with number 1\n      +\t\tKEY=FINGERPRINT\n      +\t\tSTATUS=G\n      +\t\tNONCE_STATUS=OK\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify and show ssh signatures' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\t(\n      +\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n      +\t\tdo\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'detect fudged ssh signature' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit cat-file tag seventh-signed >raw &&\n      +\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n      +\tgit hash-object -w -t tag forged1 >forged1.tag &&\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\t(\n      +\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n      +\t\tdo\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tgit verify-tag --raw sixth-signed 2>actual &&\n      +\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n      +\t! grep \"${BAD_SIGNATURE}\" actual &&\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify multiple tags ssh' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\ttags=\"seventh-signed sixth-signed\" &&\n      +\tfor i in $tags\n      +\tdo\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n     -+\ttest_config gpg.ssh.keyring \"${SIGNING_KEYRING}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect <<-\\EOF &&\n      +\ttagname : fourth-signed\n      +\tEOF\n  2:  2f8452f6570 !  9:  13f6c229bd1 ssh signing: add documentation\n     @@ Documentation/config/gpg.txt: gpg.minTrustLevel::\n       * `fully`\n       * `ultimate`\n      +\n     -+gpg.ssh.keyring::\n     -+\tA file containing all valid SSH public signing keys.\n     -+\tSimilar to an .ssh/authorized_keys file.\n     ++gpg.ssh.allowedSignersFile::\n     ++\tA file containing ssh public keys which you are willing to trust.\n     ++\tThe file consists of one or more lines of principals followed by an ssh\n     ++\tpublic key.\n     ++\te.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...\n      +\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n     -+\tIf a signing key is found in this file then the trust level will\n     -+\tbe set to \"fully\". Otherwise if the key is not present\n     -+\tbut the signature is still valid then the trust level will be \"undefined\".\n     ++\tThe principal is only used to identify the key and is available when\n     ++\tverifying a signature.\n     +++\n     ++SSH has no concept of trust levels like gpg does. To be able to differentiate\n     ++between valid signatures and trusted signatures the trust level of a signature\n     ++verification is set to `fully` when the public key is present in the allowedSignersFile.\n     ++Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.\n     ++Otherwise valid but untrusted signatures will still verify but show no principal\n     ++name of the signer.\n     +++\n     ++This file can be set to a location outside of the repository and every developer\n     ++maintains their own trust store. A central repository server could generate this\n     ++file automatically from ssh keys with push access to verify the code against.\n     ++In a corporate setting this file is probably generated at a global location\n     ++from automation that already handles developer ssh keys.\n     +++\n     ++A repository that only allows signed commits can store the file\n     ++in the repository itself using a path relative to the top-level of the working tree.\n     ++This way only committers with an already valid key can add or change keys in the keyring.\n     +++\n     ++Using a SSH CA key with the cert-authority option\n     ++(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n      +\n     -+\tThis file can be set to a location outside of the repository\n     -+\tand every developer maintains their own trust store.\n     -+\tA central repository server could generate this file automatically\n     -+\tfrom ssh keys with push\taccess to verify the code against.\n     -+\tIn a corporate setting this file is probably generated at a global location\n     -+\tfrom some automation that already handles developer ssh keys.\n     -+\n     -+\tA repository that is only allowing signed commits can store the file\n     -+\tin the repository itself using a relative path. This way only committers\n     -+\twith an already valid key can add or change keys in the keyring.\n     -+\n     -+\tUsing a SSH CA key with the cert-authority option\n     -+\t(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n     -+\n     -+\tTo revoke a key place the public key without the principal into the\n     -+\trevocationKeyring.\n     -+\n     -+gpg.ssh.revocationKeyring::\n     ++gpg.ssh.revocationFile::\n      +\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n      +\tSee ssh-keygen(1) for details.\n      +\tIf a public key is found in this file then it will always be treated\n\n-- \ngitgitgadget\n"},{"id":"430493","messageId":"b4b0e2bac1c7f8680877f3eae176b2335b607975.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:08Z","receivedAt":"2021-07-19T13:33:25Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nOpenssh v8.2p1 added some new options to ssh-keygen for signature\ncreation and verification. These allow us to use ssh keys for git\nsignatures easily.\n\nIn our corporate environment we use PIV x509 Certs on Yubikeys for email\nsigning/encryption and ssh keys which I think is quite common\n(at least for the email part). This way we can establish the correct\ntrust for the SSH Keys without setting up a separate GPG Infrastructure\n(which is still quite painful for users) or implementing x509 signing\nsupport for git (which lacks good forwarding mechanisms).\nUsing ssh agent forwarding makes this feature easily usable in todays\ndevelopment environments where code is often checked out in remote VMs / containers.\nIn such a setup the keyring & revocationKeyring can be centrally\ngenerated from the x509 CA information and distributed to the users.\n\nTo be able to implement new signing formats this commit:\n - makes the sigc structure more generic by renaming \"gpg_output\" to\n   \"output\"\n - introduces function pointers in the gpg_format structure to call\n   format specific signing and verification functions\n - moves format detection from verify_signed_buffer into the check_signature\n   api function and calls the format specific verify\n - renames and wraps sign_buffer to handle format specific signing logic\n   as well\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n fmt-merge-msg.c |   6 +--\n gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------\n gpg-interface.h |   2 +-\n log-tree.c      |   8 ++--\n pretty.c        |   4 +-\n 5 files changed, 74 insertions(+), 50 deletions(-)\n\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 0f66818e0f8..fb300bb4b67 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -526,11 +526,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tbuf = payload.buf;\n \t\t\tlen = payload.len;\n \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n-\t\t\t\t\t sig.len, &sigc) &&\n-\t\t\t\t!sigc.gpg_output)\n+\t\t\t\t\t    sig.len, &sigc) &&\n+\t\t\t    !sigc.output)\n \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n \t\t\telse\n-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n \t\t}\n \t\tsignature_check_clear(&sigc);\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..31cf4ba3938 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -15,6 +15,12 @@ struct gpg_format {\n \tconst char *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n+\tint (*verify_signed_buffer)(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n+\n static struct gpg_format gpg_format[] = {\n-\t{ .name = \"openpgp\", .program = \"gpg\",\n-\t  .verify_args = openpgp_verify_args,\n-\t  .sigs = openpgp_sigs\n+\t{\n+\t\t.name = \"openpgp\",\n+\t\t.program = \"gpg\",\n+\t\t.verify_args = openpgp_verify_args,\n+\t\t.sigs = openpgp_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n-\t{ .name = \"x509\", .program = \"gpgsm\",\n-\t  .verify_args = x509_verify_args,\n-\t  .sigs = x509_sigs\n+\t{\n+\t\t.name = \"x509\",\n+\t\t.program = \"gpgsm\",\n+\t\t.verify_args = x509_verify_args,\n+\t\t.sigs = x509_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n };\n \n@@ -72,7 +93,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\n-\tFREE_AND_NULL(sigc->gpg_output);\n+\tFREE_AND_NULL(sigc->output);\n \tFREE_AND_NULL(sigc->gpg_status);\n \tFREE_AND_NULL(sigc->signer);\n \tFREE_AND_NULL(sigc->key);\n@@ -257,16 +278,16 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n-static int verify_signed_buffer(const char *payload, size_t payload_size,\n-\t\t\t\tconst char *signature, size_t signature_size,\n-\t\t\t\tstruct strbuf *gpg_output,\n-\t\t\t\tstruct strbuf *gpg_status)\n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n-\tstruct gpg_format *fmt;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf gpg_stdout = STRBUF_INIT;\n+\tstruct strbuf gpg_stderr = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -279,10 +300,6 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\treturn -1;\n \t}\n \n-\tfmt = get_format_by_sig(signature);\n-\tif (!fmt)\n-\t\tBUG(\"bad signature '%s'\", signature);\n-\n \tstrvec_push(&gpg.args, fmt->program);\n \tstrvec_pushv(&gpg.args, fmt->verify_args);\n \tstrvec_pushl(&gpg.args,\n@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\t     \"--verify\", temp->filename.buf, \"-\",\n \t\t     NULL);\n \n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n-\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n+\tret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,\n+\t\t\t   &gpg_stderr, 0);\n \tsigchain_pop(SIGPIPE);\n \n \tdelete_tempfile(&temp);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n-\tstrbuf_release(&buf); /* no matter it was used or not */\n+\tret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tsigc->output = strbuf_detach(&gpg_stderr, NULL);\n+\tsigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);\n+\n+\tparse_gpg_output(sigc);\n+\n+\tstrbuf_release(&gpg_stdout);\n+\tstrbuf_release(&gpg_stderr);\n \n \treturn ret;\n }\n@@ -309,35 +330,32 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n-\tstruct strbuf gpg_output = STRBUF_INIT;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct gpg_format *fmt;\n \tint status;\n \n \tsigc->result = 'N';\n \tsigc->trust_level = -1;\n \n-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n-\t\t\t\t      &gpg_output, &gpg_status);\n-\tif (status && !gpg_output.len)\n-\t\tgoto out;\n-\tsigc->payload = xmemdupz(payload, plen);\n-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tfmt = get_format_by_sig(signature);\n+\tif (!fmt)\n+\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n+\n+\tstatus = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,\n+\t\t\t\t\t   slen);\n+\n+\tif (status && !sigc->output)\n+\t\treturn !!status;\n+\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n- out:\n-\tstrbuf_release(&gpg_status);\n-\tstrbuf_release(&gpg_output);\n-\n \treturn !!status;\n }\n \n void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n {\n-\tconst char *output = flags & GPG_VERIFY_RAW ?\n-\t\tsigc->gpg_status : sigc->gpg_output;\n+\tconst char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :\n+\t\t\t\t\t\t\t    sigc->output;\n \n \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n \t\tfputs(sigc->payload, stdout);\n@@ -441,6 +459,12 @@ const char *get_signing_key(void)\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n+{\n+\treturn use_format->sign_buffer(buffer, signature, signing_key);\n+}\n+\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t    const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..feac4decf8b 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -17,7 +17,7 @@ enum signature_trust_level {\n \n struct signature_check {\n \tchar *payload;\n-\tchar *gpg_output;\n+\tchar *output;\n \tchar *gpg_status;\n \n \t/*\ndiff --git a/log-tree.c b/log-tree.c\nindex 7b823786c2c..20af9bd1c82 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit)\n \n \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n \t\t\t\t signature.len, &sigc);\n-\tif (status && !sigc.gpg_output)\n+\tif (status && !sigc.output)\n \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n \telse\n-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n+\t\tshow_sig_lines(opt, status, sigc.output);\n \tsignature_check_clear(&sigc);\n \n  out:\n@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,\n \t\t/* could have a good signature */\n \t\tstatus = check_signature(payload.buf, payload.len,\n \t\t\t\t\t signature.buf, signature.len, &sigc);\n-\t\tif (sigc.gpg_output)\n-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n+\t\tif (sigc.output)\n+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n \t\telse\n \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n \t\tsignature_check_clear(&sigc);\ndiff --git a/pretty.c b/pretty.c\nindex b1ecd039cef..daa71394efd 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n \t\tswitch (placeholder[1]) {\n \t\tcase 'G':\n-\t\t\tif (c->signature_check.gpg_output)\n-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n+\t\t\tif (c->signature_check.output)\n+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n \t\t\tbreak;\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n-- \ngitgitgadget\n\n"},{"id":"430495","messageId":"2c75adee8e1d6147c5be1b3b0832cc90d44ba6df.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:09Z","receivedAt":"2021-07-19T13:33:30Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nimplements the actual sign_buffer_ssh operation and move some shared\ncleanup code into a strbuf function\n\nSet gpg.format = ssh and user.signingkey to either a ssh public key\nstring (like from an authorized_keys file), or a ssh key file.\nIf the key file or the config value itself contains only a public key\nthen the private key needs to be available via ssh-agent.\n\ngpg.ssh.program can be set to an alternative location of ssh-keygen.\nA somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\nthis feature. Since only ssh-keygen is needed it can this way be\ninstalled seperately without upgrading your system openssh packages.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---\n 1 file changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 31cf4ba3938..a086123754d 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = { NULL };\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n \n static struct gpg_format gpg_format[] = {\n \t{\n@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t},\n+\t{\n+\t\t.name = \"ssh\",\n+\t\t.program = \"ssh-keygen\",\n+\t\t.verify_args = ssh_verify_args,\n+\t\t.sigs = ssh_sigs,\n+\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.sign_buffer = sign_buffer_ssh\n+\t},\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -463,12 +482,26 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+static void strbuf_trim_trailing_cr(struct strbuf *buffer, int offset)\n+{\n+\tsize_t i, j;\n+\n+\tfor (i = j = offset; i < buffer->len; i++) {\n+\t\tif (buffer->buf[i] != '\\r') {\n+\t\t\tif (i != j)\n+\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n+\t\t\tj++;\n+\t\t}\n+\t}\n+\tstrbuf_setlen(buffer, j);\n+}\n+\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t    const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\n-\tsize_t i, j, bottom;\n+\tsize_t bottom;\n \tstruct strbuf gpg_status = STRBUF_INIT;\n \n \tstrvec_pushl(&gpg.args,\n@@ -494,13 +527,101 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\treturn error(_(\"gpg failed to sign the data\"));\n \n \t/* Strip CR from the line endings, in case we are on Windows. */\n-\tfor (i = j = bottom; i < signature->len; i++)\n-\t\tif (signature->buf[i] != '\\r') {\n-\t\t\tif (i != j)\n-\t\t\t\tsignature->buf[j] = signature->buf[i];\n-\t\t\tj++;\n-\t\t}\n-\tstrbuf_setlen(signature, j);\n+\tstrbuf_trim_trailing_cr(signature, bottom);\n \n \treturn 0;\n }\n+\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key)\n+{\n+\tstruct child_process signer = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tsize_t bottom;\n+\tstruct strbuf signer_stderr = STRBUF_INIT;\n+\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n+\tchar *ssh_signing_key_file = NULL;\n+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n+\n+\tif (!signing_key || signing_key[0] == '\\0')\n+\t\treturn error(\n+\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n+\n+\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\t/* A literal ssh key */\n+\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\tif (!temp)\n+\t\t\treturn error_errno(\n+\t\t\t\t_(\"could not create temporary file\"));\n+\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) <\n+\t\t\t    0 ||\n+\t\t    close_tempfile_gently(temp) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n+\t\t\t\t    temp->filename.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tssh_signing_key_file = temp->filename.buf;\n+\t} else {\n+\t\t/* We assume a file */\n+\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n+\tif (!buffer_file) {\n+\t\terror_errno(_(\"could not create temporary file\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tstrvec_pushl(&signer.args, use_format->program, \"-Y\", \"sign\", \"-n\",\n+\t\t     \"git\", \"-f\", ssh_signing_key_file,\n+\t\t     buffer_file->filename.buf, NULL);\n+\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n+\tsigchain_pop(SIGPIPE);\n+\n+\tif (ret && strstr(signer_stderr.buf, \"usage:\")) {\n+\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (ret) {\n+\t\terror(\"%s\", signer_stderr.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tbottom = signature->len;\n+\n+\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n+\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n+\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n+\t\terror_errno(\n+\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n+\t\t\tssh_signature_filename.buf);\n+\t}\n+\tunlink_or_warn(ssh_signature_filename.buf);\n+\n+\tif (ret) {\n+\t\terror(_(\"ssh failed to sign the data\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/* Strip CR from the line endings, in case we are on Windows. */\n+\tstrbuf_trim_trailing_cr(signature, bottom);\n+\n+out:\n+\tif (temp)\n+\t\tdelete_tempfile(&temp);\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&signer_stderr);\n+\tstrbuf_release(&ssh_signature_filename);\n+\treturn ret;\n+}\n-- \ngitgitgadget\n\n"},{"id":"430496","messageId":"1ec5c06cbe9c0b3ceded291822431baa1564485e.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:10Z","receivedAt":"2021-07-19T13:33:31Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nif user.signingkey is not set and a ssh signature is requested we call\nssh-add -L and use the first key we get\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 26 +++++++++++++++++++++++++-\n 1 file changed, 25 insertions(+), 1 deletion(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex a086123754d..35e584b94ef 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+/* Returns the first public key from an ssh-agent to use for signing */\n+static char *get_default_ssh_signing_key(void)\n+{\n+\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf key_stdout = STRBUF_INIT;\n+\tstruct strbuf **keys;\n+\n+\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n+\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n+\tif (!ret) {\n+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n+\t\tif (keys[0])\n+\t\t\treturn strbuf_detach(keys[0], NULL);\n+\t}\n+\n+\tstrbuf_release(&key_stdout);\n+\treturn \"\";\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_default_ssh_signing_key();\n+\t} else {\n+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n+\t}\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n-- \ngitgitgadget\n\n"},{"id":"430497","messageId":"ec6931082ee6dc3a5820696d0db512faa88c256f.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 4/9] ssh signing: provide a textual representation of the signing key","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:11Z","receivedAt":"2021-07-19T13:33:32Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nfor ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\nin push certs and textual output we prefer the ssh fingerprint instead.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++\n gpg-interface.h |  6 ++++++\n send-pack.c     |  8 ++++----\n 3 files changed, 56 insertions(+), 4 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 35e584b94ef..2c6eaf47d0f 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+static char *get_ssh_key_fingerprint(const char *signing_key)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n+\tstruct strbuf **fingerprint;\n+\n+\t/*\n+\t * With SSH Signing this can contain a filename or a public key\n+\t * For textual representation we usually want a fingerprint\n+\t */\n+\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n+\t\tret = pipe_command(&ssh_keygen, signing_key,\n+\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t} else {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\",\n+\t\t\t     configured_signing_key, NULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t}\n+\n+\tif (!!ret)\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n+\tif (!fingerprint[1])\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\treturn strbuf_detach(fingerprint[1], NULL);\n+}\n+\n /* Returns the first public key from an ssh-agent to use for signing */\n static char *get_default_ssh_signing_key(void)\n {\n@@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)\n \treturn \"\";\n }\n \n+/* Returns a textual but unique representation ot the signing key */\n+const char *get_signing_key_id(void)\n+{\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_ssh_key_fingerprint(get_signing_key());\n+\t} else {\n+\t\t/* GPG/GPGSM only store a key id on this variable */\n+\t\treturn get_signing_key();\n+\t}\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex feac4decf8b..beefacbb1e9 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+\n+/*\n+ * Returns a textual unique representation of the signing key in use\n+ * Either a GPG KeyID or a SSH Key Fingerprint\n+ */\n+const char *get_signing_key_id(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\ndiff --git a/send-pack.c b/send-pack.c\nindex 9cb9f716509..191fc6da544 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n {\n \tconst struct ref *ref;\n \tstruct string_list_item *item;\n-\tchar *signing_key = xstrdup(get_signing_key());\n+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n \tconst char *cp, *np;\n \tstruct strbuf cert = STRBUF_INIT;\n \tint update_seen = 0;\n \n \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n \tdatestamp(&cert);\n \tstrbuf_addch(&cert, '\\n');\n \tif (args->url && *args->url) {\n@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, get_signing_key()))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tpacket_buf_write(req_buf, \"push-cert-end\\n\");\n \n free_return:\n-\tfree(signing_key);\n+\tfree(signing_key_id);\n \tstrbuf_release(&cert);\n \treturn update_seen;\n }\n-- \ngitgitgadget\n\n"},{"id":"430498","messageId":"4436cb3a1224fdfeeee50c2d97961c50a346c337.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:12Z","receivedAt":"2021-07-19T13:33:33Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nto verify a ssh signature we first call ssh-keygen -Y find-principal to\nlook up the signing principal by their public key from the\nallowedSignersFile. If the key is found then we do a verify. Otherwise\nwe only validate the signature but can not verify the signers identity.\n\nVerification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\nSIGNERS\") which contains valid public keys and a principal (usually\nuser@domain). Depending on the environment this file can be managed by\nthe individual developer or for example generated by the central\nrepository server from known ssh keys with push access. If the\nrepository only allows signed commits / pushes then the file can even be\nstored inside it.\n\nTo revoke a key put the public key without the principal prefix into\ngpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n\"KEY REVOCATION LISTS\"). The same considerations about who to trust for\nverification as with the allowedSignersFile apply.\n\nUsing SSH CA Keys with these files is also possible. Add\n\"cert-authority\" as key option between the principal and the key to mark\nit as a CA and all keys signed by it as valid for this CA.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n builtin/receive-pack.c |   2 +\n gpg-interface.c        | 174 ++++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 175 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513ac..62b11c5f3a4 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n {\n \tint status = parse_hide_refs_config(var, value, \"receive\");\n \n+\tgit_gpg_config(var, value, NULL);\n+\n \tif (status)\n \t\treturn status;\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 2c6eaf47d0f..761aa91d648 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -3,11 +3,13 @@\n #include \"config.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n+#include \"dir.h\"\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+static const char *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -51,6 +53,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n@@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.program = \"ssh-keygen\",\n \t\t.verify_args = ssh_verify_args,\n \t\t.sigs = ssh_sigs,\n-\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n \t\t.sign_buffer = sign_buffer_ssh\n \t},\n };\n@@ -343,6 +349,160 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \treturn ret;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tconst char *line, *principal, *search;\n+\n+\t/*\n+\t * ssh-keysign output should be:\n+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n+\t * or for valid but unknown keys:\n+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n+\t */\n+\tsigc->result = 'B';\n+\tsigc->trust_level = TRUST_NEVER;\n+\n+\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n+\n+\tif (skip_prefix(line, \"Good \\\"git\\\" signature for \", &line)) {\n+\t\t/* Valid signature and known principal */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_FULLY;\n+\n+\t\t/* Search for the last \"with\" to get the full principal */\n+\t\tprincipal = line;\n+\t\tdo {\n+\t\t\tsearch = strstr(line, \" with \");\n+\t\t\tif (search)\n+\t\t\t\tline = search + 1;\n+\t\t} while (search != NULL);\n+\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n+\t\t/* Valid signature, but key unknown */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_UNDEFINED;\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t}\n+}\n+\n+static const char *get_ssh_allowed_signers(void)\n+{\n+\tif (ssh_allowed_signers)\n+\t\treturn ssh_allowed_signers;\n+\n+\tdie(\"gpg.ssh.allowedSignersFile needs to be configured and exist for validation\");\n+}\n+\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tstruct tempfile *temp;\n+\tint ret;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n+\n+\ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n+\tif (!temp)\n+\t\treturn error_errno(_(\"could not create temporary file\"));\n+\tif (write_in_full(temp->fd, signature, signature_size) < 0 ||\n+\t    close_tempfile_gently(temp) < 0) {\n+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n+\t\t\t    temp->filename.buf);\n+\t\tdelete_tempfile(&temp);\n+\t\treturn -1;\n+\t}\n+\n+\t/* Find the principal from the signers */\n+\tstrvec_pushl(&ssh_keygen.args, fmt->program, \"-Y\", \"find-principals\",\n+\t\t     \"-f\", get_ssh_allowed_signers(), \"-s\", temp->filename.buf,\n+\t\t     NULL);\n+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n+\t\t\t   &ssh_keygen_err, 0);\n+\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n+\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n+\t\treturn ret;\n+\t}\n+\tif (ret || !ssh_keygen_out.len) {\n+\t\t/* We did not find a matching principal in the allowedSigners - Check\n+\t\t * without validation */\n+\t\tchild_process_init(&ssh_keygen);\n+\t\tstrvec_pushl(&ssh_keygen.args, fmt->program, \"-Y\",\n+\t\t\t     \"check-novalidate\", \"-n\", \"git\", \"-s\",\n+\t\t\t     temp->filename.buf, NULL);\n+\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t} else {\n+\t\t/* Check every principal we found (one per line) */\n+\t\tfor (line = ssh_keygen_out.buf; *line;\n+\t\t     line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n+\n+\t\t\ttrust_size = strcspn(line, \"\\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n+\n+\t\t\tchild_process_init(&ssh_keygen);\n+\t\t\tstrbuf_release(&ssh_keygen_out);\n+\t\t\tstrbuf_release(&ssh_keygen_err);\n+\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n+\t\t\t/* We found principals - Try with each until we find a\n+\t\t\t * match */\n+\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\", \"-n\",\n+\t\t\t\t     \"git\", \"-f\", get_ssh_allowed_signers(),\n+\t\t\t\t     \"-I\", principal, \"-s\", temp->filename.buf,\n+\t\t\t\t     NULL);\n+\n+\t\t\tif (ssh_revocation_file) {\n+\t\t\t\tif (file_exists(ssh_revocation_file)) {\n+\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\",\n+\t\t\t\t\t\t     ssh_revocation_file, NULL);\n+\t\t\t\t} else {\n+\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"),\n+\t\t\t\t\t\tssh_revocation_file);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n+\n+\t\t\tFREE_AND_NULL(principal);\n+\n+\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n+\t\t\tif (ret == 0)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n+\tsigc->gpg_status = xstrdup(sigc->output);\n+\n+\tparse_ssh_output(sigc);\n+\n+\tdelete_tempfile(&temp);\n+\tstrbuf_release(&ssh_keygen_out);\n+\tstrbuf_release(&ssh_keygen_err);\n+\n+\treturn ret;\n+}\n+\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n@@ -453,6 +613,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationFile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \n-- \ngitgitgadget\n\n"},{"id":"430499","messageId":"06a76e64b353a190ac9f387e2593dde829166ebb.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 6/9] ssh signing: add test prereqs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:13Z","receivedAt":"2021-07-19T13:33:34Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\ngenerate some ssh keys and a allowedSignersFile for testing\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/lib-gpg.sh | 27 +++++++++++++++++++++++++++\n 1 file changed, 27 insertions(+)\n\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\nindex 9fc5241228e..b4fbcad4bf3 100644\n--- a/t/lib-gpg.sh\n+++ b/t/lib-gpg.sh\n@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 '\n \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n '\n \n+test_lazy_prereq GPGSSH '\n+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n+\ttest $? != 127 || exit 1\n+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n+\ttest $? = 0 || exit 1;\n+\tmkdir -p \"${GNUPGHOME}\" &&\n+\tchmod 0700 \"${GNUPGHOME}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n+\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"\\\\\\\"principal with number \\\" NR \\\"\\\\\\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n+'\n+\n+SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n+SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n+SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n+SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n+SIGNING_KEY_PASSPHRASE=\"super_secret\"\n+SIGNING_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n+\n+GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n+GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n+KEY_NOT_TRUSTED=\"No principal matched\"\n+BAD_SIGNATURE=\"Signature verification failed\"\n+\n sanitize_pgp() {\n \tperl -ne '\n \t\t/^-----END PGP/ and $in_pgp = 0;\n-- \ngitgitgadget\n\n"},{"id":"430500","messageId":"4dc5572083b4e08f704f06af8ad4649335a96581.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 7/9] ssh signing: duplicate t7510 tests for commits","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:14Z","receivedAt":"2021-07-19T13:33:35Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++\n 1 file changed, 398 insertions(+)\n create mode 100755 t/t7527-signed-commit-ssh.sh\n\ndiff --git a/t/t7527-signed-commit-ssh.sh b/t/t7527-signed-commit-ssh.sh\nnew file mode 100755\nindex 00000000000..e2c48f69e6d\n--- /dev/null\n+++ b/t/t7527-signed-commit-ssh.sh\n@@ -0,0 +1,398 @@\n+#!/bin/sh\n+\n+test_description='ssh signed commit tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed commits' '\n+\ttest_oid_cache <<-\\EOF &&\n+\theader sha1:gpgsig\n+\theader sha256:gpgsig-sha256\n+\tEOF\n+\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -S -m initial &&\n+\tgit tag initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -S -m second &&\n+\tgit tag second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n+\tgit tag fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag fourth-signed &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n+\tgit tag fifth-signed &&\n+\n+\tgit config commit.gpgsign false &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag sixth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n+\tgit tag seventh-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n+\tgit tag seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${SIGNING_KEY_UNTRUSTED}\" &&\n+\tgit tag eighth-signed-alt &&\n+\n+\t# commit.gpgsign is still on but this must not be signed\n+\techo 9 | git commit-tree HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag ninth-unsigned $(cat oid) &&\n+\t# explicit -S of course must sign.\n+\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag tenth-signed $(cat oid) &&\n+\n+\t# --gpg-sign[=<key-id>] must sign.\n+\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag eleventh-signed $(cat oid) &&\n+\techo 12 | git commit-tree --gpg-sign=\"${SIGNING_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag twelfth-signed-alt $(cat oid)\n+'\n+\n+test_expect_success GPGSSH 'verify and show signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed \\\n+\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n+\t\t\televenth-signed\n+\t\tdo\n+\t\t\tgit verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n+\t\t\tseventh-unsigned ninth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n+\t\tdo\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit verify-commit eighth-signed-alt 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\tgrep \"${KEY_NOT_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n+\tgit cat-file commit fourth-signed >output &&\n+\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n+'\n+\n+test_expect_success GPGSSH 'show signed commit with signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit show -s initial >commit &&\n+\tgit show -s --show-signature initial >show &&\n+\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n+\tgit cat-file commit initial >cat &&\n+\tgrep -v -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n+\tgrep -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n+\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n+\ttest_cmp show.commit commit &&\n+\ttest_cmp show.gpg verify.2 &&\n+\ttest_cmp cat.commit verify.1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t commit forged1 >forged1.commit &&\n+\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature with NUL' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tcat raw >forged2 &&\n+\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n+\tgit hash-object -w -t commit forged2 >forged2.commit &&\n+\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual2 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual2\n+'\n+\n+test_expect_success GPGSSH 'amending already signed commit' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit checkout fourth-signed^0 &&\n+\tgit commit --amend -S --no-edit &&\n+\tgit verify-commit HEAD &&\n+\tgit show -s --show-signature HEAD >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual\n+'\n+\n+test_expect_success GPGSSH 'show good signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tG\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show bad signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\tB\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tU\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tundefined\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tfully\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show lack of signature with custom format' '\n+\tcat >expect <<-\\EOF &&\n+\tN\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config log.showsignature true &&\n+\tgit show initial >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'check config gpg.format values' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.format ssh &&\n+\tgit commit -S --amend -m \"success\" &&\n+\ttest_config gpg.format OpEnPgP &&\n+\ttest_must_fail git commit -S --amend -m \"fail\"\n+'\n+\n+test_expect_failure GPGSSH 'detect fudged commit with double signature (TODO)' '\n+\tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n+\tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n+\t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n+\tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n+\tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n+\tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n+\t\tdouble-combined.asc > double-gpgsig &&\n+\tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n+\tgit hash-object -w -t commit double-commit >double-commit.commit &&\n+\ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n+\tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n+\tgrep \"BAD signature from\" double-actual &&\n+\tgrep \"Good signature from\" double-actual\n+'\n+\n+test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '\n+\tcat >expect <<-\\EOF &&\n+\tE\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '\n+\tgit init multiply-signed &&\n+\tcd multiply-signed &&\n+\ttest_commit first &&\n+\techo 1 >second &&\n+\tgit add second &&\n+\ttree=$(git write-tree) &&\n+\tparent=$(git rev-parse HEAD^{commit}) &&\n+\tgit commit --gpg-sign -m second &&\n+\tgit cat-file commit HEAD &&\n+\t# Avoid trailing whitespace.\n+\tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n+\tQtree $tree\n+\tQparent $parent\n+\tQauthor A U Thor <author@example.com> 1112912653 -0700\n+\tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n+\tQgpgsig -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n+\tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n+\tQ =tQ0N\n+\tQ -----END PGP SIGNATURE-----\n+\tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n+\tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n+\tQ =pIwP\n+\tQ -----END PGP SIGNATURE-----\n+\tQ\n+\tQsecond\n+\tEOF\n+\thead=$(git hash-object -t commit -w commit) &&\n+\tgit reset --hard $head &&\n+\tgit verify-commit $head 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"430501","messageId":"275dd8a1013cc27559f0ac40a4fcda3b6f5a6d1a.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 8/9] ssh signing: add more tests for logs, tags & push certs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:15Z","receivedAt":"2021-07-19T13:33:36Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t4202-log.sh                   |  23 +++++\n t/t5534-push-signed.sh           | 101 +++++++++++++++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++\n 3 files changed, 285 insertions(+)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 39e746fbcbe..afd7f2516ee 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' '\n \tgit commit -S -m signed_commit\n '\n \n+test_expect_success GPGSSH 'setup sshkey signed branch' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n+\tgit checkout -b signed-ssh main &&\n+\techo foo >foo &&\n+\tgit add foo &&\n+\tgit commit -S -m signed_commit\n+'\n+\n test_expect_success GPGSM 'log x509 fingerprint' '\n \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPGSSH 'log ssh key fingerprint' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature' '\n \tgit log --graph --show-signature -n1 signed >actual &&\n \tgrep \"^| gpg: Signature made\" actual &&\n@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' '\n \tgrep \"^| gpgsm: Good signature\" actual\n '\n \n+test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature for merged tag' '\n \ttest_when_finished \"git reset --hard && git checkout main\" &&\n \tgit checkout -b plain main &&\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex bba768f5ded..d590249b995 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n \t# First, invoke receive-pack with dummy input to obtain its preamble.\n \tprepare_dst &&\n@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n+\ttest_config gpg.format ssh &&\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config user.email hasnokey@nowhere.com &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"\" &&\n+\t(\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git push --signed dst noop ff +noff\n+\t) &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'failed atomic push does not execute GPG' '\n \tprepare_dst &&\n \tgit -C dst config receive.certnonceseed sekrit &&\ndiff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh\nnew file mode 100755\nindex 00000000000..05bf520a332\n--- /dev/null\n+++ b/t/t7031-verify-tag-signed-ssh.sh\n@@ -0,0 +1,161 @@\n+#!/bin/sh\n+\n+test_description='signed tag tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed tags ssh' '\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -m initial &&\n+\tgit tag -s -m initial initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -m second &&\n+\tgit tag -s -m second second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag -s -m merge merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag -s -m fourth fourth-signed &&\n+\n+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n+\tgit tag fifth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag -a -m sixth sixth-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n+\tgit tag -m seventh -s seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth &&\n+\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify and show ssh signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'detect fudged ssh signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file tag seventh-signed >raw &&\n+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit verify-tag --raw sixth-signed 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\techo sixth-signed OK\n+'\n+\n+test_expect_success GPGSSH 'verify multiple tags ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttags=\"seventh-signed sixth-signed\" &&\n+\tfor i in $tags\n+\tdo\n+\t\tgit verify-tag -v --raw $i || return 1\n+\tdone >expect.stdout 2>expect.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n+\tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n+\ttest_cmp expect.stdout actual.stdout &&\n+\ttest_cmp expect.stderr actual.stderr\n+'\n+\n+test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\ttagname : fourth-signed\n+\tEOF\n+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently - ssh' '\n+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n+\ttest_must_be_empty actual-forged\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"430502","messageId":"13f6c229bd1459dcb0c8ab59c1ef22fb3430be72.1626701596.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v4 9/9] ssh signing: add documentation","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-19T13:33:16Z","receivedAt":"2021-07-19T13:33:37Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  | 39 +++++++++++++++++++++++++++++++++--\n Documentation/config/user.txt |  6 ++++++\n 2 files changed, 43 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..dc790512e86 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\n@@ -33,3 +33,38 @@ gpg.minTrustLevel::\n * `marginal`\n * `fully`\n * `ultimate`\n+\n+gpg.ssh.allowedSignersFile::\n+\tA file containing ssh public keys which you are willing to trust.\n+\tThe file consists of one or more lines of principals followed by an ssh\n+\tpublic key.\n+\te.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...\n+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n+\tThe principal is only used to identify the key and is available when\n+\tverifying a signature.\n++\n+SSH has no concept of trust levels like gpg does. To be able to differentiate\n+between valid signatures and trusted signatures the trust level of a signature\n+verification is set to `fully` when the public key is present in the allowedSignersFile.\n+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.\n+Otherwise valid but untrusted signatures will still verify but show no principal\n+name of the signer.\n++\n+This file can be set to a location outside of the repository and every developer\n+maintains their own trust store. A central repository server could generate this\n+file automatically from ssh keys with push access to verify the code against.\n+In a corporate setting this file is probably generated at a global location\n+from automation that already handles developer ssh keys.\n++\n+A repository that only allows signed commits can store the file\n+in the repository itself using a path relative to the top-level of the working tree.\n+This way only committers with an already valid key can add or change keys in the keyring.\n++\n+Using a SSH CA key with the cert-authority option\n+(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n+\n+gpg.ssh.revocationFile::\n+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n+\tSee ssh-keygen(1) for details.\n+\tIf a public key is found in this file then it will always be treated\n+\tas having trust level \"never\" and signatures will show as invalid.\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..b3c2f2c541e 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,9 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n+\tcorresponds to the private key used for signing. The private key\n+\tneeds to be available via ssh-agent. Alternatively it can be set to\n+\ta file containing a private key directly. If not set git will call\n+\t\"ssh-add -L\" and try to use the first key available.\n-- \ngitgitgadget\n"},{"id":"430558","messageId":"xmqqfsw9kim6.fsf@gitster.g","threadId":"56054","inReplyTo":"b4b0e2bac1c7f8680877f3eae176b2335b607975.1626701596.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-19T23:07:45Z","receivedAt":"2021-07-19T23:15:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 127aecfc2b0..31cf4ba3938 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -15,6 +15,12 @@ struct gpg_format {\n>  \tconst char *program;\n>  \tconst char **verify_args;\n>  \tconst char **sigs;\n> +\tint (*verify_signed_buffer)(struct signature_check *sigc,\n> +\t\t\t\t    struct gpg_format *fmt, const char *payload,\n> +\t\t\t\t    size_t payload_size, const char *signature,\n> +\t\t\t\t    size_t signature_size);\n> +\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t   const char *signing_key);\n>  };\n\nThanks for a pleasant read.  Looking good.\n"},{"id":"430559","messageId":"xmqqpmvdj1xp.fsf@gitster.g","threadId":"56054","inReplyTo":"2c75adee8e1d6147c5be1b3b0832cc90d44ba6df.1626701596.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-19T23:53:22Z","receivedAt":"2021-07-20T00:33:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> @@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {\n>  \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n>  \t\t.sign_buffer = sign_buffer_gpg,\n>  \t},\n> +\t{\n> +\t\t.name = \"ssh\",\n> +\t\t.program = \"ssh-keygen\",\n> +\t\t.verify_args = ssh_verify_args,\n> +\t\t.sigs = ssh_sigs,\n> +\t\t.verify_signed_buffer = NULL, /* TODO */\n> +\t\t.sign_buffer = sign_buffer_ssh\n> +\t},\n>  };\n\nA payload a malicious person may feed this version of Git can have a\npattern that happens to match the ssh_sigs[] string, and the code\nwill blindly try to call .verify_signed_buffer==NULL and die, no?\n\nThat is not the end of the world; as long as we know that with the\nabove \"TODO\" comment it is probably OK.\n\n> @@ -463,12 +482,26 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n>  \treturn use_format->sign_buffer(buffer, signature, signing_key);\n>  }\n>  \n> +static void strbuf_trim_trailing_cr(struct strbuf *buffer, int offset)\n> +{\n\nThis removes any and all CR, not just trimming the trailing ones, so\nthe function is misnamed. Call it remove_cr_after() perhaps?\n\nAlternatively we could tighten the implementation and strip only the\nCR that come immediately before a LF.  That would be a better longer\nterm thing to do, but because you are lifting an existing code from\nthe end of the gpg side of the thing, it may make sense to keep the\nimplementation as-is, but give it a name that is more faithful to\nwhat it actually does.  When the dust settles, we may want to\nrevisit and fix this helper function to actually trim CRLF into LF\n(and leave CR in the middle of lines intact), but I do not think it\nis urgent.  Just leaving \"NEEDSWORK: make it trim only CRs before LFs\nand rename\" comment would be OK.\n\nShouldn't the offset (aka bottom) be of type size_t?\n\nI do not recommend giving the function a name that begins with\n\"strbuf_\", as it would tempt unthinking person to suggest moving it\nto strbuf.c, but the presense of the \"offset\" thing means it will be\nklunky to reuse in other more generic contexts as a part of the\nstrbuf API.\n\n> +\tsize_t i, j;\n> +\n> +\tfor (i = j = offset; i < buffer->len; i++) {\n> +\t\tif (buffer->buf[i] != '\\r') {\n> ...\n\nNow it gets interesting ;-)\n\n> +static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t   const char *signing_key)\n> +{\n> +\tstruct child_process signer = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tsize_t bottom;\n> +\tstruct strbuf signer_stderr = STRBUF_INIT;\n> +\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n> +\tchar *ssh_signing_key_file = NULL;\n> +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n> +\n> +\tif (!signing_key || signing_key[0] == '\\0')\n> +\t\treturn error(\n> +\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n> +\n> +\tif (istarts_with(signing_key, \"ssh-\")) {\n\nIs it common in the ssh world to treat ssh- prefix as case\ninsensitive?  Not a strong objection but I tend to prefer to start\nstrict unless there is a good reason to be loose when we do not have\nto, as loosening after the fact is much easier than tightening after\nstarting with a loose definition.\n\n> +\t\t/* A literal ssh key */\n> +\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n> +\t\tif (!temp)\n> +\t\t\treturn error_errno(\n> +\t\t\t\t_(\"could not create temporary file\"));\n> +\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) <\n> +\t\t\t    0 ||\n\n\"keylen = strlen(signing_key)\" before that line, for example, could\nhave easily avoided the line-wrapping at such a place.  Wrapping at\nplaces like after ||, i.e. after an operator with a low precedence,\nwould make the code easier to follow.\n\n> +\t\t    close_tempfile_gently(temp) < 0) {\n> +\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n> +\t\t\t\t    temp->filename.buf);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tssh_signing_key_file = temp->filename.buf;\n\nIt is kind'a sad that we need a fresh temporary file every time, but\nwe can easily tell the user in the documentation that they can use a\nfile with a key in it to avoid it, so it's OK (actually, better than\nOK, as without this, we may not consume temporary files but we won't\noffer an ability to take a literal key string).\n\nIs \".git_whatever file in the current directory\" a good place to\nhave this temporary file?  I would have expected that we would use\neither $GIT_DIR, $HOME, or $TMPDIR for a thing like this (with\ndifferent pros-and-cons discussion).  At least it is consistent with\nhow a temporary file for the payload to be sign-verified is created,\nso let's leave it as-is.\n\n> +\t} else {\n> +\t\t/* We assume a file */\n> +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n> +\t}\n> +\n> +\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n> +\tif (!buffer_file) {\n> +\t\terror_errno(_(\"could not create temporary file\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n> +\t    close_tempfile_gently(buffer_file) < 0) {\n> +\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n> +\t\t\t    buffer_file->filename.buf);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tstrvec_pushl(&signer.args, use_format->program, \"-Y\", \"sign\", \"-n\",\n> +\t\t     \"git\", \"-f\", ssh_signing_key_file,\n\nWrap the line before \"-n\" to keep \"-n\" and \"git\" together, if \"git\"\nis meant as an argument to the \"-n\" option.\n\n> +\t\t     buffer_file->filename.buf, NULL);\n> +\n> +\tsigchain_push(SIGPIPE, SIG_IGN);\n> +\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n> +\tsigchain_pop(SIGPIPE);\n> +\n> +\tif (ret && strstr(signer_stderr.buf, \"usage:\")) {\n> +\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n> +\t\tgoto out;\n\nThis error message is important to give to the end users, but is it\nenough?  That is, unless \"usage:\" does not appear, we show the whole\nraw error message and that would help end users and those helping\nthem to diagnose the issue, but once the underlying program says\n\"usage:\", no matter what else it says, it is hidden by this code,\nsince we assume it is a wrong version of openssh.\n\n> +\t}\n> +\n> +\tif (ret) {\n> +\t\terror(\"%s\", signer_stderr.buf);\n> +\t\tgoto out;\n> +\t}\n\nAlso, prehaps\n\n\tif (ret) {\n\t\tif (strstr(..., \"usage\"))\n\t\t\terror(_(\"ssh-keygen -Y sign is needed...\"));\n\t\telse\n                        error(\"%s\", signer_stderr.buf);\n\t\tgoto out;\n\t}\n\nwould be easier to follow.\n\n> +\tbottom = signature->len;\n> +\n> +\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n> +\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n> +\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n\nIs it likely that signature file is smaller than 2kB?  I am just\nwondering how much we care to pick the default that is specific to\nthis codepath vs passing 0 to ask the strbuf API to use whatever\ndefault it wants to.\n\n> +\t\terror_errno(\n> +\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n> +\t\t\tssh_signature_filename.buf);\n> +\t}\n> +\tunlink_or_warn(ssh_signature_filename.buf);\n\nWait a bit.  Even when pipe_command() tells us that we failed, we\nread from ssh_signature_filename anyway?  What is going on?  And ...\n\n> +\tif (ret) {\n\n... does this ever trigger?  I thought we would have hit one of the\ntwo \"goto out\" when ret signals an error by being non-zero earlier,\nand since then nobody touched the variable so far.\n\n> +\t\terror(_(\"ssh failed to sign the data\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\t/* Strip CR from the line endings, in case we are on Windows. */\n> +\tstrbuf_trim_trailing_cr(signature, bottom);\n> +\n> +out:\n> +\tif (temp)\n> +\t\tdelete_tempfile(&temp);\n> +\tif (buffer_file)\n> +\t\tdelete_tempfile(&buffer_file);\n\nIt is clear that the latter one was holding the contents of the\nbuffer to be signed, but reminding the readers what \"temp\" was about\nwould be a good move.  Perhaps renaming the variable to \"key_file\"\nor something may help?\n\n> +\tstrbuf_release(&signer_stderr);\n> +\tstrbuf_release(&ssh_signature_filename);\n> +\treturn ret;\n> +}\n\nLooking good, except for the \"when does 'ret' get updated?\nshouldn't we refrain from reading the resulting buffer when it is\nset?\" question.\n\nThanks for a pleasant read.\n\n"},{"id":"430560","messageId":"xmqqh7gpizu2.fsf@gitster.g","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-20T00:38:45Z","receivedAt":"2021-07-20T01:02:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n>  create mode 100755 t/t7527-signed-commit-ssh.sh\n\nAs the number 7527 is already in use by another topic in 'seen',\nthis new one must be relocated to coexist with them.\n"},{"id":"430620","messageId":"b673b68a-6525-44fd-0c1b-7943eb794184@gigacodes.de","threadId":"56054","inReplyTo":"xmqqpmvdj1xp.fsf@gitster.g","subject":"Re: [PATCH v4 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-20T12:26:21Z","receivedAt":"2021-07-20T12:28:36Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\nOn 20.07.21 01:53, Junio C Hamano wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> @@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {\n>>   \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n>>   \t\t.sign_buffer = sign_buffer_gpg,\n>>   \t},\n>> +\t{\n>> +\t\t.name = \"ssh\",\n>> +\t\t.program = \"ssh-keygen\",\n>> +\t\t.verify_args = ssh_verify_args,\n>> +\t\t.sigs = ssh_sigs,\n>> +\t\t.verify_signed_buffer = NULL, /* TODO */\n>> +\t\t.sign_buffer = sign_buffer_ssh\n>> +\t},\n>>   };\n> A payload a malicious person may feed this version of Git can have a\n> pattern that happens to match the ssh_sigs[] string, and the code\n> will blindly try to call .verify_signed_buffer==NULL and die, no?\n>\n> That is not the end of the world; as long as we know that with the\n> above \"TODO\" comment it is probably OK.\nI thought about adding an if(!fmt->sign) BUG() but since these callbacks \nare static it shoud really only be an issue between patches of the set.\n>\n>> @@ -463,12 +482,26 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n>>   \treturn use_format->sign_buffer(buffer, signature, signing_key);\n>>   }\n>>   \n>> +static void strbuf_trim_trailing_cr(struct strbuf *buffer, int offset)\n>> +{\n> This removes any and all CR, not just trimming the trailing ones, so\n> the function is misnamed. Call it remove_cr_after() perhaps?\n>\n> Alternatively we could tighten the implementation and strip only the\n> CR that come immediately before a LF.  That would be a better longer\n> term thing to do, but because you are lifting an existing code from\n> the end of the gpg side of the thing, it may make sense to keep the\n> implementation as-is, but give it a name that is more faithful to\n> what it actually does.  When the dust settles, we may want to\n> revisit and fix this helper function to actually trim CRLF into LF\n> (and leave CR in the middle of lines intact), but I do not think it\n> is urgent.  Just leaving \"NEEDSWORK: make it trim only CRs before LFs\n> and rename\" comment would be OK.\nAgreed. I've renamed it and added the comment.\n>\n> Shouldn't the offset (aka bottom) be of type size_t?\nfixed\n>> +static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n>> +\t\t\t   const char *signing_key)\n>> +{\n>> +\tstruct child_process signer = CHILD_PROCESS_INIT;\n>> +\tint ret = -1;\n>> +\tsize_t bottom;\n>> +\tstruct strbuf signer_stderr = STRBUF_INIT;\n>> +\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n>> +\tchar *ssh_signing_key_file = NULL;\n>> +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n>> +\n>> +\tif (!signing_key || signing_key[0] == '\\0')\n>> +\t\treturn error(\n>> +\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n>> +\n>> +\tif (istarts_with(signing_key, \"ssh-\")) {\n> Is it common in the ssh world to treat ssh- prefix as case\n> insensitive?  Not a strong objection but I tend to prefer to start\n> strict unless there is a good reason to be loose when we do not have\n> to, as loosening after the fact is much easier than tightening after\n> starting with a loose definition.\nI don't think so. I will make it case sensitive.\n>\n>> +\t\t/* A literal ssh key */\n>> +\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n>> +\t\tif (!temp)\n>> +\t\t\treturn error_errno(\n>> +\t\t\t\t_(\"could not create temporary file\"));\n>> +\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) <\n>> +\t\t\t    0 ||\n> \"keylen = strlen(signing_key)\" before that line, for example, could\n> have easily avoided the line-wrapping at such a place.  Wrapping at\n> places like after ||, i.e. after an operator with a low precedence,\n> would make the code easier to follow.\nagreed.\n>\n>> +\t\t    close_tempfile_gently(temp) < 0) {\n>> +\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n>> +\t\t\t\t    temp->filename.buf);\n>> +\t\t\tgoto out;\n>> +\t\t}\n>> +\t\tssh_signing_key_file = temp->filename.buf;\n> It is kind'a sad that we need a fresh temporary file every time, but\n> we can easily tell the user in the documentation that they can use a\n> file with a key in it to avoid it, so it's OK (actually, better than\n> OK, as without this, we may not consume temporary files but we won't\n> offer an ability to take a literal key string).\n>\n> Is \".git_whatever file in the current directory\" a good place to\n> have this temporary file?  I would have expected that we would use\n> either $GIT_DIR, $HOME, or $TMPDIR for a thing like this (with\n> different pros-and-cons discussion).  At least it is consistent with\n> how a temporary file for the payload to be sign-verified is created,\n> so let's leave it as-is.\nIntuitively i thought just using mks_tempfile_t() would choose a good \ndir for such files.\n>\n>> +\t} else {\n>> +\t\t/* We assume a file */\n>> +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n>> +\t}\n>> +\n>> +\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n>> +\tif (!buffer_file) {\n>> +\t\terror_errno(_(\"could not create temporary file\"));\n>> +\t\tgoto out;\n>> +\t}\n>> +\n>> +\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n>> +\t    close_tempfile_gently(buffer_file) < 0) {\n>> +\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n>> +\t\t\t    buffer_file->filename.buf);\n>> +\t\tgoto out;\n>> +\t}\n>> +\n>> +\tstrvec_pushl(&signer.args, use_format->program, \"-Y\", \"sign\", \"-n\",\n>> +\t\t     \"git\", \"-f\", ssh_signing_key_file,\n> Wrap the line before \"-n\" to keep \"-n\" and \"git\" together, if \"git\"\n> is meant as an argument to the \"-n\" option.\ndone. some things clang-format can't really understand. overall it is \nquite helpful but a few things i still had to reformat.\n>\n>> +\t\t     buffer_file->filename.buf, NULL);\n>> +\n>> +\tsigchain_push(SIGPIPE, SIG_IGN);\n>> +\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n>> +\tsigchain_pop(SIGPIPE);\n>> +\n>> +\tif (ret && strstr(signer_stderr.buf, \"usage:\")) {\n>> +\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n>> +\t\tgoto out;\n> This error message is important to give to the end users, but is it\n> enough?  That is, unless \"usage:\" does not appear, we show the whole\n> raw error message and that would help end users and those helping\n> them to diagnose the issue, but once the underlying program says\n> \"usage:\", no matter what else it says, it is hidden by this code,\n> since we assume it is a wrong version of openssh.\n>\n>> +\t}\n>> +\n>> +\tif (ret) {\n>> +\t\terror(\"%s\", signer_stderr.buf);\n>> +\t\tgoto out;\n>> +\t}\n> Also, prehaps\n>\n> \tif (ret) {\n> \t\tif (strstr(..., \"usage\"))\n> \t\t\terror(_(\"ssh-keygen -Y sign is needed...\"));\n> \t\telse\n>                          error(\"%s\", signer_stderr.buf);\n> \t\tgoto out;\n> \t}\n>\n> would be easier to follow.\n\nI have changed this to:\nif (ret) {\n     if (strstr(..., \"usage\"))\n         error(_(\"ssh-keygen -Y sign is needed...\"));\n\n     error(\"%s\", signer_stderr.buf);\n     goto out;\n}\nand removed the if (ret) further down in the function that had no effect.\n\nI had removed the raw stderr output from verify & sign because it \nbecomes quite unreadable when doing a \"git log --show-signature\" with \nssh signatures present and not support in ssh for it.\nI think in case of signing the full output is good. The user is taking \nan active action (wanting to sign something) so we should give them all \nthe help we can when things go wrong.\nThe output for verification is debatable. The config might have a \n\"log.showSignature\" for verifying gpg signatures and when ssh signatures \nshow up we should tell them it's not supported on their setup but \nprobably not showing endless lines of errors when they do a \"git log\". A \n\"git verify-commit\" might be a different case. But code-path-wise this \nis the same thing at the moment.\n\n>> +\t\terror(_(\"ssh failed to sign the data\"));\n>> +\t\tgoto out;\n>> +\t}\n>> +\n>> +\t/* Strip CR from the line endings, in case we are on Windows. */\n>> +\tstrbuf_trim_trailing_cr(signature, bottom);\n>> +\n>> +out:\n>> +\tif (temp)\n>> +\t\tdelete_tempfile(&temp);\n>> +\tif (buffer_file)\n>> +\t\tdelete_tempfile(&buffer_file);\n> It is clear that the latter one was holding the contents of the\n> buffer to be signed, but reminding the readers what \"temp\" was about\n> would be a good move.  Perhaps renaming the variable to \"key_file\"\n> or something may help?\nrenamed to \"key_file\"\n>\n>> +\tstrbuf_release(&signer_stderr);\n>> +\tstrbuf_release(&ssh_signature_filename);\n>> +\treturn ret;\n>> +}\n> Looking good, except for the \"when does 'ret' get updated?\n> shouldn't we refrain from reading the resulting buffer when it is\n> set?\" question.\n>\n> Thanks for a pleasant read.\nThanks a lot for your support with this!\n>\n\n"},{"id":"431300","messageId":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v4.git.git.1626701596.gitgitgadget@gmail.com","subject":"[PATCH v5 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:35Z","receivedAt":"2021-07-27T13:15:50Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"I have added support for using keyfiles directly, lots of tests and\ngenerally cleaned up the signing & verification code a lot.\n\nI can still rename things from being gpg specific to a more general\n\"signing\" but thats rather cosmetic. Also i'm not sure if i named the new\ntest files correctly.\n\nopenssh 8.7 will add valid-after, valid-before options to the allowed keys\nkeyring. This allows us to pass the commit timestamp to the verification\ncall and make key rollover possible and still be able to verify older\ncommits. Set valid-after=NOW when adding your key to the keyring and set\nvalid-before to make it fail if used after a certain date. Software like\ngitolite/github or corporate automation can do this automatically when ssh\npush keys are addded / removed I will add this feature in a follow up patch\nafterwards.\n\nv3 addresses some issues & refactoring and splits the large commit into\nseveral smaller ones.\n\nv4:\n\n * restructures and cleans up the whole patch set - patches build on its own\n   now and commit messages try to explain whats going on\n * got rid of the if branches and used callback functions in the format\n   struct\n * fixed a bug with whitespace in principal identifiers that required a\n   rewrite of the parse_ssh_output function\n * rewrote documentation to be more clear - also renamed keyring back to\n   allowedSignersFile\n\nv5:\n\n * moved t7527 to t7528 to not collide with another patch in \"seen\"\n * clean up return logic for failed signing & verification\n * some minor renames / reformatting to make things clearer\n\nFabian Stelzer (9):\n  ssh signing: preliminary refactoring and clean-up\n  ssh signing: add ssh signature format and signing using ssh keys\n  ssh signing: retrieve a default key from ssh-agent\n  ssh signing: provide a textual representation of the signing key\n  ssh signing: parse ssh-keygen output and verify signatures\n  ssh signing: add test prereqs\n  ssh signing: duplicate t7510 tests for commits\n  ssh signing: add more tests for logs, tags & push certs\n  ssh signing: add documentation\n\n Documentation/config/gpg.txt     |  39 ++-\n Documentation/config/user.txt    |   6 +\n builtin/receive-pack.c           |   2 +\n fmt-merge-msg.c                  |   6 +-\n gpg-interface.c                  | 490 +++++++++++++++++++++++++++----\n gpg-interface.h                  |   8 +-\n log-tree.c                       |   8 +-\n pretty.c                         |   4 +-\n send-pack.c                      |   8 +-\n t/lib-gpg.sh                     |  27 ++\n t/t4202-log.sh                   |  23 ++\n t/t5534-push-signed.sh           | 101 +++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++\n t/t7528-signed-commit-ssh.sh     | 398 +++++++++++++++++++++++++\n 14 files changed, 1216 insertions(+), 65 deletions(-)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\n\nbase-commit: eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v5\nPull-Request: https://github.com/git/git/pull/1041\n\nRange-diff vs v4:\n\n  1:  b4b0e2bac1c =  1:  7c8502c65b8 ssh signing: preliminary refactoring and clean-up\n  2:  2c75adee8e1 !  2:  f05bab16096 ssh signing: add ssh signature format and signing using ssh keys\n     @@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature\n       \treturn use_format->sign_buffer(buffer, signature, signing_key);\n       }\n       \n     -+static void strbuf_trim_trailing_cr(struct strbuf *buffer, int offset)\n     ++/*\n     ++ * Strip CR from the line endings, in case we are on Windows.\n     ++ * NEEDSWORK: make it trim only CRs before LFs and rename\n     ++ */\n     ++static void remove_cr_after(struct strbuf *buffer, size_t offset)\n      +{\n      +\tsize_t i, j;\n      +\n     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf\n      -\t\t\tj++;\n      -\t\t}\n      -\tstrbuf_setlen(signature, j);\n     -+\tstrbuf_trim_trailing_cr(signature, bottom);\n     ++\tremove_cr_after(signature, bottom);\n       \n       \treturn 0;\n       }\n     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf\n      +{\n      +\tstruct child_process signer = CHILD_PROCESS_INIT;\n      +\tint ret = -1;\n     -+\tsize_t bottom;\n     ++\tsize_t bottom, keylen;\n      +\tstruct strbuf signer_stderr = STRBUF_INIT;\n     -+\tstruct tempfile *temp = NULL, *buffer_file = NULL;\n     ++\tstruct tempfile *key_file = NULL, *buffer_file = NULL;\n      +\tchar *ssh_signing_key_file = NULL;\n      +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n      +\n     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf\n      +\t\treturn error(\n      +\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n      +\n     -+\tif (istarts_with(signing_key, \"ssh-\")) {\n     ++\tif (starts_with(signing_key, \"ssh-\")) {\n      +\t\t/* A literal ssh key */\n     -+\t\ttemp = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n     -+\t\tif (!temp)\n     ++\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n     ++\t\tif (!key_file)\n      +\t\t\treturn error_errno(\n      +\t\t\t\t_(\"could not create temporary file\"));\n     -+\t\tif (write_in_full(temp->fd, signing_key, strlen(signing_key)) <\n     -+\t\t\t    0 ||\n     -+\t\t    close_tempfile_gently(temp) < 0) {\n     ++\t\tkeylen = strlen(signing_key);\n     ++\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n     ++\t\t    close_tempfile_gently(key_file) < 0) {\n      +\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n     -+\t\t\t\t    temp->filename.buf);\n     ++\t\t\t\t    key_file->filename.buf);\n      +\t\t\tgoto out;\n      +\t\t}\n     -+\t\tssh_signing_key_file = temp->filename.buf;\n     ++\t\tssh_signing_key_file = key_file->filename.buf;\n      +\t} else {\n      +\t\t/* We assume a file */\n      +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf\n      +\t\tgoto out;\n      +\t}\n      +\n     -+\tstrvec_pushl(&signer.args, use_format->program, \"-Y\", \"sign\", \"-n\",\n     -+\t\t     \"git\", \"-f\", ssh_signing_key_file,\n     -+\t\t     buffer_file->filename.buf, NULL);\n     ++\tstrvec_pushl(&signer.args, use_format->program,\n     ++\t\t     \"-Y\", \"sign\",\n     ++\t\t     \"-n\", \"git\",\n     ++\t\t     \"-f\", ssh_signing_key_file,\n     ++\t\t     buffer_file->filename.buf,\n     ++\t\t     NULL);\n      +\n      +\tsigchain_push(SIGPIPE, SIG_IGN);\n      +\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n      +\tsigchain_pop(SIGPIPE);\n      +\n     -+\tif (ret && strstr(signer_stderr.buf, \"usage:\")) {\n     -+\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n     -+\t\tgoto out;\n     -+\t}\n     -+\n      +\tif (ret) {\n     ++\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n     ++\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n     ++\n      +\t\terror(\"%s\", signer_stderr.buf);\n      +\t\tgoto out;\n      +\t}\n     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf\n      +\n      +\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n      +\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n     -+\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 2048) < 0) {\n     ++\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {\n      +\t\terror_errno(\n      +\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n      +\t\t\tssh_signature_filename.buf);\n      +\t}\n      +\tunlink_or_warn(ssh_signature_filename.buf);\n      +\n     -+\tif (ret) {\n     -+\t\terror(_(\"ssh failed to sign the data\"));\n     -+\t\tgoto out;\n     -+\t}\n     -+\n      +\t/* Strip CR from the line endings, in case we are on Windows. */\n     -+\tstrbuf_trim_trailing_cr(signature, bottom);\n     ++\tremove_cr_after(signature, bottom);\n      +\n      +out:\n     -+\tif (temp)\n     -+\t\tdelete_tempfile(&temp);\n     ++\tif (key_file)\n     ++\t\tdelete_tempfile(&key_file);\n      +\tif (buffer_file)\n      +\t\tdelete_tempfile(&buffer_file);\n      +\tstrbuf_release(&signer_stderr);\n  3:  1ec5c06cbe9 =  3:  071e6173d8e ssh signing: retrieve a default key from ssh-agent\n  4:  ec6931082ee =  4:  7d1d131ff5b ssh signing: provide a textual representation of the signing key\n  5:  4436cb3a122 !  5:  725764018ce ssh signing: parse ssh-keygen output and verify signatures\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\t}\n      +}\n      +\n     -+static const char *get_ssh_allowed_signers(void)\n     -+{\n     -+\tif (ssh_allowed_signers)\n     -+\t\treturn ssh_allowed_signers;\n     -+\n     -+\tdie(\"gpg.ssh.allowedSignersFile needs to be configured and exist for validation\");\n     -+}\n     -+\n      +static int verify_ssh_signed_buffer(struct signature_check *sigc,\n      +\t\t\t\t    struct gpg_format *fmt, const char *payload,\n      +\t\t\t\t    size_t payload_size, const char *signature,\n      +\t\t\t\t    size_t signature_size)\n      +{\n      +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n     -+\tstruct tempfile *temp;\n     -+\tint ret;\n     ++\tstruct tempfile *buffer_file;\n     ++\tint ret = -1;\n      +\tconst char *line;\n      +\tsize_t trust_size;\n      +\tchar *principal;\n      +\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n      +\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n      +\n     -+\ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n     -+\tif (!temp)\n     ++\tif (!ssh_allowed_signers) {\n     ++\t\terror(_(\"gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification\"));\n     ++\t\treturn -1;\n     ++\t}\n     ++\n     ++\tbuffer_file = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n     ++\tif (!buffer_file)\n      +\t\treturn error_errno(_(\"could not create temporary file\"));\n     -+\tif (write_in_full(temp->fd, signature, signature_size) < 0 ||\n     -+\t    close_tempfile_gently(temp) < 0) {\n     ++\tif (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||\n     ++\t    close_tempfile_gently(buffer_file) < 0) {\n      +\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n     -+\t\t\t    temp->filename.buf);\n     -+\t\tdelete_tempfile(&temp);\n     ++\t\t\t    buffer_file->filename.buf);\n     ++\t\tdelete_tempfile(&buffer_file);\n      +\t\treturn -1;\n      +\t}\n      +\n      +\t/* Find the principal from the signers */\n     -+\tstrvec_pushl(&ssh_keygen.args, fmt->program, \"-Y\", \"find-principals\",\n     -+\t\t     \"-f\", get_ssh_allowed_signers(), \"-s\", temp->filename.buf,\n     ++\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n     ++\t\t     \"-Y\", \"find-principals\",\n     ++\t\t     \"-f\", ssh_allowed_signers,\n     ++\t\t     \"-s\", buffer_file->filename.buf,\n      +\t\t     NULL);\n      +\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n      +\t\t\t   &ssh_keygen_err, 0);\n      +\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n      +\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n     -+\t\treturn ret;\n     ++\t\tgoto out;\n      +\t}\n      +\tif (ret || !ssh_keygen_out.len) {\n      +\t\t/* We did not find a matching principal in the allowedSigners - Check\n      +\t\t * without validation */\n      +\t\tchild_process_init(&ssh_keygen);\n     -+\t\tstrvec_pushl(&ssh_keygen.args, fmt->program, \"-Y\",\n     -+\t\t\t     \"check-novalidate\", \"-n\", \"git\", \"-s\",\n     -+\t\t\t     temp->filename.buf, NULL);\n     ++\t\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n     ++\t\t\t     \"-Y\", \"check-novalidate\",\n     ++\t\t\t     \"-n\", \"git\",\n     ++\t\t\t     \"-s\", buffer_file->filename.buf,\n     ++\t\t\t     NULL);\n      +\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n      +\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n      +\t} else {\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n      +\t\t\t/* We found principals - Try with each until we find a\n      +\t\t\t * match */\n     -+\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\", \"-n\",\n     -+\t\t\t\t     \"git\", \"-f\", get_ssh_allowed_signers(),\n     -+\t\t\t\t     \"-I\", principal, \"-s\", temp->filename.buf,\n     ++\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\",\n     ++\t\t\t\t     \"-n\", \"git\",\n     ++\t\t\t\t     \"-f\", ssh_allowed_signers,\n     ++\t\t\t\t     \"-I\", principal,\n     ++\t\t\t\t     \"-s\", buffer_file->filename.buf,\n      +\t\t\t\t     NULL);\n      +\n      +\t\t\tif (ssh_revocation_file) {\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\n      +\tparse_ssh_output(sigc);\n      +\n     -+\tdelete_tempfile(&temp);\n     ++out:\n     ++\tif (buffer_file)\n     ++\t\tdelete_tempfile(&buffer_file);\n      +\tstrbuf_release(&ssh_keygen_out);\n      +\tstrbuf_release(&ssh_keygen_err);\n      +\n  6:  06a76e64b35 =  6:  eb677b1b6a8 ssh signing: add test prereqs\n  7:  4dc5572083b !  7:  c877951df23 ssh signing: duplicate t7510 tests for commits\n     @@ Commit message\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     - ## t/t7527-signed-commit-ssh.sh (new) ##\n     + ## t/t7528-signed-commit-ssh.sh (new) ##\n      @@\n      +#!/bin/sh\n      +\n  8:  275dd8a1013 =  8:  60265e8c399 ssh signing: add more tests for logs, tags & push certs\n  9:  13f6c229bd1 =  9:  f758ce0ade4 ssh signing: add documentation\n\n-- \ngitgitgadget\n"},{"id":"431301","messageId":"7c8502c65b833e7e563a833b592f6932421b1056.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:36Z","receivedAt":"2021-07-27T13:15:50Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nOpenssh v8.2p1 added some new options to ssh-keygen for signature\ncreation and verification. These allow us to use ssh keys for git\nsignatures easily.\n\nIn our corporate environment we use PIV x509 Certs on Yubikeys for email\nsigning/encryption and ssh keys which I think is quite common\n(at least for the email part). This way we can establish the correct\ntrust for the SSH Keys without setting up a separate GPG Infrastructure\n(which is still quite painful for users) or implementing x509 signing\nsupport for git (which lacks good forwarding mechanisms).\nUsing ssh agent forwarding makes this feature easily usable in todays\ndevelopment environments where code is often checked out in remote VMs / containers.\nIn such a setup the keyring & revocationKeyring can be centrally\ngenerated from the x509 CA information and distributed to the users.\n\nTo be able to implement new signing formats this commit:\n - makes the sigc structure more generic by renaming \"gpg_output\" to\n   \"output\"\n - introduces function pointers in the gpg_format structure to call\n   format specific signing and verification functions\n - moves format detection from verify_signed_buffer into the check_signature\n   api function and calls the format specific verify\n - renames and wraps sign_buffer to handle format specific signing logic\n   as well\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n fmt-merge-msg.c |   6 +--\n gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------\n gpg-interface.h |   2 +-\n log-tree.c      |   8 ++--\n pretty.c        |   4 +-\n 5 files changed, 74 insertions(+), 50 deletions(-)\n\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 0f66818e0f8..fb300bb4b67 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -526,11 +526,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tbuf = payload.buf;\n \t\t\tlen = payload.len;\n \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n-\t\t\t\t\t sig.len, &sigc) &&\n-\t\t\t\t!sigc.gpg_output)\n+\t\t\t\t\t    sig.len, &sigc) &&\n+\t\t\t    !sigc.output)\n \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n \t\t\telse\n-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n \t\t}\n \t\tsignature_check_clear(&sigc);\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..31cf4ba3938 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -15,6 +15,12 @@ struct gpg_format {\n \tconst char *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n+\tint (*verify_signed_buffer)(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n+\n static struct gpg_format gpg_format[] = {\n-\t{ .name = \"openpgp\", .program = \"gpg\",\n-\t  .verify_args = openpgp_verify_args,\n-\t  .sigs = openpgp_sigs\n+\t{\n+\t\t.name = \"openpgp\",\n+\t\t.program = \"gpg\",\n+\t\t.verify_args = openpgp_verify_args,\n+\t\t.sigs = openpgp_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n-\t{ .name = \"x509\", .program = \"gpgsm\",\n-\t  .verify_args = x509_verify_args,\n-\t  .sigs = x509_sigs\n+\t{\n+\t\t.name = \"x509\",\n+\t\t.program = \"gpgsm\",\n+\t\t.verify_args = x509_verify_args,\n+\t\t.sigs = x509_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n };\n \n@@ -72,7 +93,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\n-\tFREE_AND_NULL(sigc->gpg_output);\n+\tFREE_AND_NULL(sigc->output);\n \tFREE_AND_NULL(sigc->gpg_status);\n \tFREE_AND_NULL(sigc->signer);\n \tFREE_AND_NULL(sigc->key);\n@@ -257,16 +278,16 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n-static int verify_signed_buffer(const char *payload, size_t payload_size,\n-\t\t\t\tconst char *signature, size_t signature_size,\n-\t\t\t\tstruct strbuf *gpg_output,\n-\t\t\t\tstruct strbuf *gpg_status)\n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n-\tstruct gpg_format *fmt;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf gpg_stdout = STRBUF_INIT;\n+\tstruct strbuf gpg_stderr = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -279,10 +300,6 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\treturn -1;\n \t}\n \n-\tfmt = get_format_by_sig(signature);\n-\tif (!fmt)\n-\t\tBUG(\"bad signature '%s'\", signature);\n-\n \tstrvec_push(&gpg.args, fmt->program);\n \tstrvec_pushv(&gpg.args, fmt->verify_args);\n \tstrvec_pushl(&gpg.args,\n@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\t     \"--verify\", temp->filename.buf, \"-\",\n \t\t     NULL);\n \n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n-\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n+\tret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,\n+\t\t\t   &gpg_stderr, 0);\n \tsigchain_pop(SIGPIPE);\n \n \tdelete_tempfile(&temp);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n-\tstrbuf_release(&buf); /* no matter it was used or not */\n+\tret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tsigc->output = strbuf_detach(&gpg_stderr, NULL);\n+\tsigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);\n+\n+\tparse_gpg_output(sigc);\n+\n+\tstrbuf_release(&gpg_stdout);\n+\tstrbuf_release(&gpg_stderr);\n \n \treturn ret;\n }\n@@ -309,35 +330,32 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n-\tstruct strbuf gpg_output = STRBUF_INIT;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct gpg_format *fmt;\n \tint status;\n \n \tsigc->result = 'N';\n \tsigc->trust_level = -1;\n \n-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n-\t\t\t\t      &gpg_output, &gpg_status);\n-\tif (status && !gpg_output.len)\n-\t\tgoto out;\n-\tsigc->payload = xmemdupz(payload, plen);\n-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tfmt = get_format_by_sig(signature);\n+\tif (!fmt)\n+\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n+\n+\tstatus = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,\n+\t\t\t\t\t   slen);\n+\n+\tif (status && !sigc->output)\n+\t\treturn !!status;\n+\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n- out:\n-\tstrbuf_release(&gpg_status);\n-\tstrbuf_release(&gpg_output);\n-\n \treturn !!status;\n }\n \n void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n {\n-\tconst char *output = flags & GPG_VERIFY_RAW ?\n-\t\tsigc->gpg_status : sigc->gpg_output;\n+\tconst char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :\n+\t\t\t\t\t\t\t    sigc->output;\n \n \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n \t\tfputs(sigc->payload, stdout);\n@@ -441,6 +459,12 @@ const char *get_signing_key(void)\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n+{\n+\treturn use_format->sign_buffer(buffer, signature, signing_key);\n+}\n+\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t    const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..feac4decf8b 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -17,7 +17,7 @@ enum signature_trust_level {\n \n struct signature_check {\n \tchar *payload;\n-\tchar *gpg_output;\n+\tchar *output;\n \tchar *gpg_status;\n \n \t/*\ndiff --git a/log-tree.c b/log-tree.c\nindex 7b823786c2c..20af9bd1c82 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit)\n \n \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n \t\t\t\t signature.len, &sigc);\n-\tif (status && !sigc.gpg_output)\n+\tif (status && !sigc.output)\n \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n \telse\n-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n+\t\tshow_sig_lines(opt, status, sigc.output);\n \tsignature_check_clear(&sigc);\n \n  out:\n@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,\n \t\t/* could have a good signature */\n \t\tstatus = check_signature(payload.buf, payload.len,\n \t\t\t\t\t signature.buf, signature.len, &sigc);\n-\t\tif (sigc.gpg_output)\n-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n+\t\tif (sigc.output)\n+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n \t\telse\n \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n \t\tsignature_check_clear(&sigc);\ndiff --git a/pretty.c b/pretty.c\nindex b1ecd039cef..daa71394efd 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n \t\tswitch (placeholder[1]) {\n \t\tcase 'G':\n-\t\t\tif (c->signature_check.gpg_output)\n-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n+\t\t\tif (c->signature_check.output)\n+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n \t\t\tbreak;\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n-- \ngitgitgadget\n\n"},{"id":"431302","messageId":"f05bab16096c080891ee8f7e179eecce7f32e839.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:37Z","receivedAt":"2021-07-27T13:15:53Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nimplements the actual sign_buffer_ssh operation and move some shared\ncleanup code into a strbuf function\n\nSet gpg.format = ssh and user.signingkey to either a ssh public key\nstring (like from an authorized_keys file), or a ssh key file.\nIf the key file or the config value itself contains only a public key\nthen the private key needs to be available via ssh-agent.\n\ngpg.ssh.program can be set to an alternative location of ssh-keygen.\nA somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\nthis feature. Since only ssh-keygen is needed it can this way be\ninstalled seperately without upgrading your system openssh packages.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---\n 1 file changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 31cf4ba3938..c131977b347 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = { NULL };\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n \n static struct gpg_format gpg_format[] = {\n \t{\n@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t},\n+\t{\n+\t\t.name = \"ssh\",\n+\t\t.program = \"ssh-keygen\",\n+\t\t.verify_args = ssh_verify_args,\n+\t\t.sigs = ssh_sigs,\n+\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.sign_buffer = sign_buffer_ssh\n+\t},\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+/*\n+ * Strip CR from the line endings, in case we are on Windows.\n+ * NEEDSWORK: make it trim only CRs before LFs and rename\n+ */\n+static void remove_cr_after(struct strbuf *buffer, size_t offset)\n+{\n+\tsize_t i, j;\n+\n+\tfor (i = j = offset; i < buffer->len; i++) {\n+\t\tif (buffer->buf[i] != '\\r') {\n+\t\t\tif (i != j)\n+\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n+\t\t\tj++;\n+\t\t}\n+\t}\n+\tstrbuf_setlen(buffer, j);\n+}\n+\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t    const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\n-\tsize_t i, j, bottom;\n+\tsize_t bottom;\n \tstruct strbuf gpg_status = STRBUF_INIT;\n \n \tstrvec_pushl(&gpg.args,\n@@ -494,13 +531,97 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\treturn error(_(\"gpg failed to sign the data\"));\n \n \t/* Strip CR from the line endings, in case we are on Windows. */\n-\tfor (i = j = bottom; i < signature->len; i++)\n-\t\tif (signature->buf[i] != '\\r') {\n-\t\t\tif (i != j)\n-\t\t\t\tsignature->buf[j] = signature->buf[i];\n-\t\t\tj++;\n-\t\t}\n-\tstrbuf_setlen(signature, j);\n+\tremove_cr_after(signature, bottom);\n \n \treturn 0;\n }\n+\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key)\n+{\n+\tstruct child_process signer = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tsize_t bottom, keylen;\n+\tstruct strbuf signer_stderr = STRBUF_INIT;\n+\tstruct tempfile *key_file = NULL, *buffer_file = NULL;\n+\tchar *ssh_signing_key_file = NULL;\n+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n+\n+\tif (!signing_key || signing_key[0] == '\\0')\n+\t\treturn error(\n+\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n+\n+\tif (starts_with(signing_key, \"ssh-\")) {\n+\t\t/* A literal ssh key */\n+\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\tif (!key_file)\n+\t\t\treturn error_errno(\n+\t\t\t\t_(\"could not create temporary file\"));\n+\t\tkeylen = strlen(signing_key);\n+\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n+\t\t    close_tempfile_gently(key_file) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n+\t\t\t\t    key_file->filename.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tssh_signing_key_file = key_file->filename.buf;\n+\t} else {\n+\t\t/* We assume a file */\n+\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n+\tif (!buffer_file) {\n+\t\terror_errno(_(\"could not create temporary file\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tstrvec_pushl(&signer.args, use_format->program,\n+\t\t     \"-Y\", \"sign\",\n+\t\t     \"-n\", \"git\",\n+\t\t     \"-f\", ssh_signing_key_file,\n+\t\t     buffer_file->filename.buf,\n+\t\t     NULL);\n+\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n+\tsigchain_pop(SIGPIPE);\n+\n+\tif (ret) {\n+\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n+\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n+\n+\t\terror(\"%s\", signer_stderr.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tbottom = signature->len;\n+\n+\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n+\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n+\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {\n+\t\terror_errno(\n+\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n+\t\t\tssh_signature_filename.buf);\n+\t}\n+\tunlink_or_warn(ssh_signature_filename.buf);\n+\n+\t/* Strip CR from the line endings, in case we are on Windows. */\n+\tremove_cr_after(signature, bottom);\n+\n+out:\n+\tif (key_file)\n+\t\tdelete_tempfile(&key_file);\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&signer_stderr);\n+\tstrbuf_release(&ssh_signature_filename);\n+\treturn ret;\n+}\n-- \ngitgitgadget\n\n"},{"id":"431303","messageId":"7d1d131ff5b43559c8a750ebdfd6faaba93c1ad1.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 4/9] ssh signing: provide a textual representation of the signing key","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:39Z","receivedAt":"2021-07-27T13:15:57Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nfor ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\nin push certs and textual output we prefer the ssh fingerprint instead.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++\n gpg-interface.h |  6 ++++++\n send-pack.c     |  8 ++++----\n 3 files changed, 56 insertions(+), 4 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 3afacb48900..ec48a37b6cc 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+static char *get_ssh_key_fingerprint(const char *signing_key)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n+\tstruct strbuf **fingerprint;\n+\n+\t/*\n+\t * With SSH Signing this can contain a filename or a public key\n+\t * For textual representation we usually want a fingerprint\n+\t */\n+\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n+\t\tret = pipe_command(&ssh_keygen, signing_key,\n+\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t} else {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\",\n+\t\t\t     configured_signing_key, NULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t}\n+\n+\tif (!!ret)\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n+\tif (!fingerprint[1])\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\treturn strbuf_detach(fingerprint[1], NULL);\n+}\n+\n /* Returns the first public key from an ssh-agent to use for signing */\n static char *get_default_ssh_signing_key(void)\n {\n@@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)\n \treturn \"\";\n }\n \n+/* Returns a textual but unique representation ot the signing key */\n+const char *get_signing_key_id(void)\n+{\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_ssh_key_fingerprint(get_signing_key());\n+\t} else {\n+\t\t/* GPG/GPGSM only store a key id on this variable */\n+\t\treturn get_signing_key();\n+\t}\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex feac4decf8b..beefacbb1e9 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+\n+/*\n+ * Returns a textual unique representation of the signing key in use\n+ * Either a GPG KeyID or a SSH Key Fingerprint\n+ */\n+const char *get_signing_key_id(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\ndiff --git a/send-pack.c b/send-pack.c\nindex 5a79e0e7110..50cca7e439b 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n {\n \tconst struct ref *ref;\n \tstruct string_list_item *item;\n-\tchar *signing_key = xstrdup(get_signing_key());\n+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n \tconst char *cp, *np;\n \tstruct strbuf cert = STRBUF_INIT;\n \tint update_seen = 0;\n \n \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n \tdatestamp(&cert);\n \tstrbuf_addch(&cert, '\\n');\n \tif (args->url && *args->url) {\n@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, get_signing_key()))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tpacket_buf_write(req_buf, \"push-cert-end\\n\");\n \n free_return:\n-\tfree(signing_key);\n+\tfree(signing_key_id);\n \tstrbuf_release(&cert);\n \treturn update_seen;\n }\n-- \ngitgitgadget\n\n"},{"id":"431304","messageId":"071e6173d8e418349d94fea97624e8cee9f1dde5.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:38Z","receivedAt":"2021-07-27T13:15:58Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nif user.signingkey is not set and a ssh signature is requested we call\nssh-add -L and use the first key we get\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 26 +++++++++++++++++++++++++-\n 1 file changed, 25 insertions(+), 1 deletion(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex c131977b347..3afacb48900 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+/* Returns the first public key from an ssh-agent to use for signing */\n+static char *get_default_ssh_signing_key(void)\n+{\n+\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf key_stdout = STRBUF_INIT;\n+\tstruct strbuf **keys;\n+\n+\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n+\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n+\tif (!ret) {\n+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n+\t\tif (keys[0])\n+\t\t\treturn strbuf_detach(keys[0], NULL);\n+\t}\n+\n+\tstrbuf_release(&key_stdout);\n+\treturn \"\";\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_default_ssh_signing_key();\n+\t} else {\n+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n+\t}\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n-- \ngitgitgadget\n\n"},{"id":"431305","messageId":"725764018ceb5bcecc748cc5169d4305ea9d7d23.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:40Z","receivedAt":"2021-07-27T13:16:00Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nto verify a ssh signature we first call ssh-keygen -Y find-principal to\nlook up the signing principal by their public key from the\nallowedSignersFile. If the key is found then we do a verify. Otherwise\nwe only validate the signature but can not verify the signers identity.\n\nVerification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\nSIGNERS\") which contains valid public keys and a principal (usually\nuser@domain). Depending on the environment this file can be managed by\nthe individual developer or for example generated by the central\nrepository server from known ssh keys with push access. If the\nrepository only allows signed commits / pushes then the file can even be\nstored inside it.\n\nTo revoke a key put the public key without the principal prefix into\ngpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n\"KEY REVOCATION LISTS\"). The same considerations about who to trust for\nverification as with the allowedSignersFile apply.\n\nUsing SSH CA Keys with these files is also possible. Add\n\"cert-authority\" as key option between the principal and the key to mark\nit as a CA and all keys signed by it as valid for this CA.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n builtin/receive-pack.c |   2 +\n gpg-interface.c        | 179 ++++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 180 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513ac..62b11c5f3a4 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n {\n \tint status = parse_hide_refs_config(var, value, \"receive\");\n \n+\tgit_gpg_config(var, value, NULL);\n+\n \tif (status)\n \t\treturn status;\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex ec48a37b6cc..703225c3cd3 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -3,11 +3,13 @@\n #include \"config.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n+#include \"dir.h\"\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+static const char *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -51,6 +53,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n@@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.program = \"ssh-keygen\",\n \t\t.verify_args = ssh_verify_args,\n \t\t.sigs = ssh_sigs,\n-\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n \t\t.sign_buffer = sign_buffer_ssh\n \t},\n };\n@@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \treturn ret;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tconst char *line, *principal, *search;\n+\n+\t/*\n+\t * ssh-keysign output should be:\n+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n+\t * or for valid but unknown keys:\n+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n+\t */\n+\tsigc->result = 'B';\n+\tsigc->trust_level = TRUST_NEVER;\n+\n+\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n+\n+\tif (skip_prefix(line, \"Good \\\"git\\\" signature for \", &line)) {\n+\t\t/* Valid signature and known principal */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_FULLY;\n+\n+\t\t/* Search for the last \"with\" to get the full principal */\n+\t\tprincipal = line;\n+\t\tdo {\n+\t\t\tsearch = strstr(line, \" with \");\n+\t\t\tif (search)\n+\t\t\t\tline = search + 1;\n+\t\t} while (search != NULL);\n+\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n+\t\t/* Valid signature, but key unknown */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_UNDEFINED;\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t}\n+}\n+\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tstruct tempfile *buffer_file;\n+\tint ret = -1;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n+\n+\tif (!ssh_allowed_signers) {\n+\t\terror(_(\"gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification\"));\n+\t\treturn -1;\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n+\tif (!buffer_file)\n+\t\treturn error_errno(_(\"could not create temporary file\"));\n+\tif (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tdelete_tempfile(&buffer_file);\n+\t\treturn -1;\n+\t}\n+\n+\t/* Find the principal from the signers */\n+\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t     \"-Y\", \"find-principals\",\n+\t\t     \"-f\", ssh_allowed_signers,\n+\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t     NULL);\n+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n+\t\t\t   &ssh_keygen_err, 0);\n+\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n+\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n+\t\tgoto out;\n+\t}\n+\tif (ret || !ssh_keygen_out.len) {\n+\t\t/* We did not find a matching principal in the allowedSigners - Check\n+\t\t * without validation */\n+\t\tchild_process_init(&ssh_keygen);\n+\t\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t\t     \"-Y\", \"check-novalidate\",\n+\t\t\t     \"-n\", \"git\",\n+\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t     NULL);\n+\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t} else {\n+\t\t/* Check every principal we found (one per line) */\n+\t\tfor (line = ssh_keygen_out.buf; *line;\n+\t\t     line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n+\n+\t\t\ttrust_size = strcspn(line, \"\\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n+\n+\t\t\tchild_process_init(&ssh_keygen);\n+\t\t\tstrbuf_release(&ssh_keygen_out);\n+\t\t\tstrbuf_release(&ssh_keygen_err);\n+\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n+\t\t\t/* We found principals - Try with each until we find a\n+\t\t\t * match */\n+\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\",\n+\t\t\t\t     \"-n\", \"git\",\n+\t\t\t\t     \"-f\", ssh_allowed_signers,\n+\t\t\t\t     \"-I\", principal,\n+\t\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t\t     NULL);\n+\n+\t\t\tif (ssh_revocation_file) {\n+\t\t\t\tif (file_exists(ssh_revocation_file)) {\n+\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\",\n+\t\t\t\t\t\t     ssh_revocation_file, NULL);\n+\t\t\t\t} else {\n+\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"),\n+\t\t\t\t\t\tssh_revocation_file);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n+\n+\t\t\tFREE_AND_NULL(principal);\n+\n+\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n+\t\t\tif (ret == 0)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n+\tsigc->gpg_status = xstrdup(sigc->output);\n+\n+\tparse_ssh_output(sigc);\n+\n+out:\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&ssh_keygen_out);\n+\tstrbuf_release(&ssh_keygen_err);\n+\n+\treturn ret;\n+}\n+\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n@@ -453,6 +618,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationFile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \n-- \ngitgitgadget\n\n"},{"id":"431306","messageId":"eb677b1b6a89dd8dd52bd5b77ba4c2799bb29ad7.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 6/9] ssh signing: add test prereqs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:41Z","receivedAt":"2021-07-27T13:16:00Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\ngenerate some ssh keys and a allowedSignersFile for testing\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/lib-gpg.sh | 27 +++++++++++++++++++++++++++\n 1 file changed, 27 insertions(+)\n\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\nindex 9fc5241228e..b4fbcad4bf3 100644\n--- a/t/lib-gpg.sh\n+++ b/t/lib-gpg.sh\n@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 '\n \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n '\n \n+test_lazy_prereq GPGSSH '\n+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n+\ttest $? != 127 || exit 1\n+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n+\ttest $? = 0 || exit 1;\n+\tmkdir -p \"${GNUPGHOME}\" &&\n+\tchmod 0700 \"${GNUPGHOME}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n+\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n+\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"\\\\\\\"principal with number \\\" NR \\\"\\\\\\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n+'\n+\n+SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n+SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n+SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n+SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n+SIGNING_KEY_PASSPHRASE=\"super_secret\"\n+SIGNING_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n+\n+GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n+GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n+KEY_NOT_TRUSTED=\"No principal matched\"\n+BAD_SIGNATURE=\"Signature verification failed\"\n+\n sanitize_pgp() {\n \tperl -ne '\n \t\t/^-----END PGP/ and $in_pgp = 0;\n-- \ngitgitgadget\n\n"},{"id":"431307","messageId":"c877951df232e69681fb04f78e0a7d77a95aa4ab.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 7/9] ssh signing: duplicate t7510 tests for commits","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:42Z","receivedAt":"2021-07-27T13:16:01Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++\n 1 file changed, 398 insertions(+)\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\ndiff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh\nnew file mode 100755\nindex 00000000000..e2c48f69e6d\n--- /dev/null\n+++ b/t/t7528-signed-commit-ssh.sh\n@@ -0,0 +1,398 @@\n+#!/bin/sh\n+\n+test_description='ssh signed commit tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed commits' '\n+\ttest_oid_cache <<-\\EOF &&\n+\theader sha1:gpgsig\n+\theader sha256:gpgsig-sha256\n+\tEOF\n+\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -S -m initial &&\n+\tgit tag initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -S -m second &&\n+\tgit tag second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n+\tgit tag fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag fourth-signed &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n+\tgit tag fifth-signed &&\n+\n+\tgit config commit.gpgsign false &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag sixth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n+\tgit tag seventh-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n+\tgit tag seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${SIGNING_KEY_UNTRUSTED}\" &&\n+\tgit tag eighth-signed-alt &&\n+\n+\t# commit.gpgsign is still on but this must not be signed\n+\techo 9 | git commit-tree HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag ninth-unsigned $(cat oid) &&\n+\t# explicit -S of course must sign.\n+\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag tenth-signed $(cat oid) &&\n+\n+\t# --gpg-sign[=<key-id>] must sign.\n+\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag eleventh-signed $(cat oid) &&\n+\techo 12 | git commit-tree --gpg-sign=\"${SIGNING_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag twelfth-signed-alt $(cat oid)\n+'\n+\n+test_expect_success GPGSSH 'verify and show signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed \\\n+\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n+\t\t\televenth-signed\n+\t\tdo\n+\t\t\tgit verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n+\t\t\tseventh-unsigned ninth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n+\t\tdo\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit verify-commit eighth-signed-alt 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\tgrep \"${KEY_NOT_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n+\tgit cat-file commit fourth-signed >output &&\n+\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n+'\n+\n+test_expect_success GPGSSH 'show signed commit with signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit show -s initial >commit &&\n+\tgit show -s --show-signature initial >show &&\n+\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n+\tgit cat-file commit initial >cat &&\n+\tgrep -v -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n+\tgrep -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n+\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n+\ttest_cmp show.commit commit &&\n+\ttest_cmp show.gpg verify.2 &&\n+\ttest_cmp cat.commit verify.1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t commit forged1 >forged1.commit &&\n+\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature with NUL' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tcat raw >forged2 &&\n+\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n+\tgit hash-object -w -t commit forged2 >forged2.commit &&\n+\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual2 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual2\n+'\n+\n+test_expect_success GPGSSH 'amending already signed commit' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit checkout fourth-signed^0 &&\n+\tgit commit --amend -S --no-edit &&\n+\tgit verify-commit HEAD &&\n+\tgit show -s --show-signature HEAD >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual\n+'\n+\n+test_expect_success GPGSSH 'show good signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tG\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show bad signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\tB\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tU\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tundefined\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tfully\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show lack of signature with custom format' '\n+\tcat >expect <<-\\EOF &&\n+\tN\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config log.showsignature true &&\n+\tgit show initial >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'check config gpg.format values' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.format ssh &&\n+\tgit commit -S --amend -m \"success\" &&\n+\ttest_config gpg.format OpEnPgP &&\n+\ttest_must_fail git commit -S --amend -m \"fail\"\n+'\n+\n+test_expect_failure GPGSSH 'detect fudged commit with double signature (TODO)' '\n+\tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n+\tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n+\t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n+\tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n+\tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n+\tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n+\t\tdouble-combined.asc > double-gpgsig &&\n+\tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n+\tgit hash-object -w -t commit double-commit >double-commit.commit &&\n+\ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n+\tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n+\tgrep \"BAD signature from\" double-actual &&\n+\tgrep \"Good signature from\" double-actual\n+'\n+\n+test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '\n+\tcat >expect <<-\\EOF &&\n+\tE\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '\n+\tgit init multiply-signed &&\n+\tcd multiply-signed &&\n+\ttest_commit first &&\n+\techo 1 >second &&\n+\tgit add second &&\n+\ttree=$(git write-tree) &&\n+\tparent=$(git rev-parse HEAD^{commit}) &&\n+\tgit commit --gpg-sign -m second &&\n+\tgit cat-file commit HEAD &&\n+\t# Avoid trailing whitespace.\n+\tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n+\tQtree $tree\n+\tQparent $parent\n+\tQauthor A U Thor <author@example.com> 1112912653 -0700\n+\tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n+\tQgpgsig -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n+\tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n+\tQ =tQ0N\n+\tQ -----END PGP SIGNATURE-----\n+\tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n+\tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n+\tQ =pIwP\n+\tQ -----END PGP SIGNATURE-----\n+\tQ\n+\tQsecond\n+\tEOF\n+\thead=$(git hash-object -t commit -w commit) &&\n+\tgit reset --hard $head &&\n+\tgit verify-commit $head 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"431308","messageId":"60265e8c399c59458262ea12e6ef1a057f0377a2.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 8/9] ssh signing: add more tests for logs, tags & push certs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:43Z","receivedAt":"2021-07-27T13:16:03Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t4202-log.sh                   |  23 +++++\n t/t5534-push-signed.sh           | 101 +++++++++++++++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++\n 3 files changed, 285 insertions(+)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 39e746fbcbe..afd7f2516ee 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' '\n \tgit commit -S -m signed_commit\n '\n \n+test_expect_success GPGSSH 'setup sshkey signed branch' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n+\tgit checkout -b signed-ssh main &&\n+\techo foo >foo &&\n+\tgit add foo &&\n+\tgit commit -S -m signed_commit\n+'\n+\n test_expect_success GPGSM 'log x509 fingerprint' '\n \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPGSSH 'log ssh key fingerprint' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature' '\n \tgit log --graph --show-signature -n1 signed >actual &&\n \tgrep \"^| gpg: Signature made\" actual &&\n@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' '\n \tgrep \"^| gpgsm: Good signature\" actual\n '\n \n+test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature for merged tag' '\n \ttest_when_finished \"git reset --hard && git checkout main\" &&\n \tgit checkout -b plain main &&\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex bba768f5ded..d590249b995 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n \t# First, invoke receive-pack with dummy input to obtain its preamble.\n \tprepare_dst &&\n@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n+\ttest_config gpg.format ssh &&\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config user.email hasnokey@nowhere.com &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"\" &&\n+\t(\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git push --signed dst noop ff +noff\n+\t) &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'failed atomic push does not execute GPG' '\n \tprepare_dst &&\n \tgit -C dst config receive.certnonceseed sekrit &&\ndiff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh\nnew file mode 100755\nindex 00000000000..05bf520a332\n--- /dev/null\n+++ b/t/t7031-verify-tag-signed-ssh.sh\n@@ -0,0 +1,161 @@\n+#!/bin/sh\n+\n+test_description='signed tag tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed tags ssh' '\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -m initial &&\n+\tgit tag -s -m initial initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -m second &&\n+\tgit tag -s -m second second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag -s -m merge merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag -s -m fourth fourth-signed &&\n+\n+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n+\tgit tag fifth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag -a -m sixth sixth-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n+\tgit tag -m seventh -s seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth &&\n+\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify and show ssh signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'detect fudged ssh signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file tag seventh-signed >raw &&\n+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit verify-tag --raw sixth-signed 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\techo sixth-signed OK\n+'\n+\n+test_expect_success GPGSSH 'verify multiple tags ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttags=\"seventh-signed sixth-signed\" &&\n+\tfor i in $tags\n+\tdo\n+\t\tgit verify-tag -v --raw $i || return 1\n+\tdone >expect.stdout 2>expect.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n+\tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n+\ttest_cmp expect.stdout actual.stdout &&\n+\ttest_cmp expect.stderr actual.stderr\n+'\n+\n+test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\ttagname : fourth-signed\n+\tEOF\n+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently - ssh' '\n+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n+\ttest_must_be_empty actual-forged\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"431309","messageId":"f758ce0ade4575ab3a8de63aaced676eea35146a.1627391744.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v5 9/9] ssh signing: add documentation","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-27T13:15:44Z","receivedAt":"2021-07-27T13:16:05Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  | 39 +++++++++++++++++++++++++++++++++--\n Documentation/config/user.txt |  6 ++++++\n 2 files changed, 43 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..dc790512e86 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\n@@ -33,3 +33,38 @@ gpg.minTrustLevel::\n * `marginal`\n * `fully`\n * `ultimate`\n+\n+gpg.ssh.allowedSignersFile::\n+\tA file containing ssh public keys which you are willing to trust.\n+\tThe file consists of one or more lines of principals followed by an ssh\n+\tpublic key.\n+\te.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...\n+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n+\tThe principal is only used to identify the key and is available when\n+\tverifying a signature.\n++\n+SSH has no concept of trust levels like gpg does. To be able to differentiate\n+between valid signatures and trusted signatures the trust level of a signature\n+verification is set to `fully` when the public key is present in the allowedSignersFile.\n+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.\n+Otherwise valid but untrusted signatures will still verify but show no principal\n+name of the signer.\n++\n+This file can be set to a location outside of the repository and every developer\n+maintains their own trust store. A central repository server could generate this\n+file automatically from ssh keys with push access to verify the code against.\n+In a corporate setting this file is probably generated at a global location\n+from automation that already handles developer ssh keys.\n++\n+A repository that only allows signed commits can store the file\n+in the repository itself using a path relative to the top-level of the working tree.\n+This way only committers with an already valid key can add or change keys in the keyring.\n++\n+Using a SSH CA key with the cert-authority option\n+(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n+\n+gpg.ssh.revocationFile::\n+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n+\tSee ssh-keygen(1) for details.\n+\tIf a public key is found in this file then it will always be treated\n+\tas having trust level \"never\" and signatures will show as invalid.\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..b3c2f2c541e 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,9 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n+\tcorresponds to the private key used for signing. The private key\n+\tneeds to be available via ssh-agent. Alternatively it can be set to\n+\ta file containing a private key directly. If not set git will call\n+\t\"ssh-add -L\" and try to use the first key available.\n-- \ngitgitgadget\n"},{"id":"431419","messageId":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v5.git.git.1627391744.gitgitgadget@gmail.com","subject":"[PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:40Z","receivedAt":"2021-07-28T19:36:54Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"I have added support for using keyfiles directly, lots of tests and\ngenerally cleaned up the signing & verification code a lot.\n\nI can still rename things from being gpg specific to a more general\n\"signing\" but thats rather cosmetic. Also i'm not sure if i named the new\ntest files correctly.\n\nopenssh 8.7 will add valid-after, valid-before options to the allowed keys\nkeyring. This allows us to pass the commit timestamp to the verification\ncall and make key rollover possible and still be able to verify older\ncommits. Set valid-after=NOW when adding your key to the keyring and set\nvalid-before to make it fail if used after a certain date. Software like\ngitolite/github or corporate automation can do this automatically when ssh\npush keys are addded / removed I will add this feature in a follow up patch\nafterwards.\n\nv3 addresses some issues & refactoring and splits the large commit into\nseveral smaller ones.\n\nv4:\n\n * restructures and cleans up the whole patch set - patches build on its own\n   now and commit messages try to explain whats going on\n * got rid of the if branches and used callback functions in the format\n   struct\n * fixed a bug with whitespace in principal identifiers that required a\n   rewrite of the parse_ssh_output function\n * rewrote documentation to be more clear - also renamed keyring back to\n   allowedSignersFile\n\nv5:\n\n * moved t7527 to t7528 to not collide with another patch in \"seen\"\n * clean up return logic for failed signing & verification\n * some minor renames / reformatting to make things clearer\n\nv6: fixed tests when using shm output dir\n\nFabian Stelzer (9):\n  ssh signing: preliminary refactoring and clean-up\n  ssh signing: add ssh signature format and signing using ssh keys\n  ssh signing: retrieve a default key from ssh-agent\n  ssh signing: provide a textual representation of the signing key\n  ssh signing: parse ssh-keygen output and verify signatures\n  ssh signing: add test prereqs\n  ssh signing: duplicate t7510 tests for commits\n  ssh signing: add more tests for logs, tags & push certs\n  ssh signing: add documentation\n\n Documentation/config/gpg.txt     |  39 ++-\n Documentation/config/user.txt    |   6 +\n builtin/receive-pack.c           |   2 +\n fmt-merge-msg.c                  |   6 +-\n gpg-interface.c                  | 490 +++++++++++++++++++++++++++----\n gpg-interface.h                  |   8 +-\n log-tree.c                       |   8 +-\n pretty.c                         |   4 +-\n send-pack.c                      |   8 +-\n t/lib-gpg.sh                     |  29 ++\n t/t4202-log.sh                   |  23 ++\n t/t5534-push-signed.sh           | 101 +++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++\n t/t7528-signed-commit-ssh.sh     | 398 +++++++++++++++++++++++++\n 14 files changed, 1218 insertions(+), 65 deletions(-)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\n\nbase-commit: eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v6\nPull-Request: https://github.com/git/git/pull/1041\n\nRange-diff vs v5:\n\n  1:  7c8502c65b8 =  1:  7c8502c65b8 ssh signing: preliminary refactoring and clean-up\n  2:  f05bab16096 =  2:  f05bab16096 ssh signing: add ssh signature format and signing using ssh keys\n  3:  071e6173d8e =  3:  071e6173d8e ssh signing: retrieve a default key from ssh-agent\n  4:  7d1d131ff5b =  4:  7d1d131ff5b ssh signing: provide a textual representation of the signing key\n  5:  725764018ce =  5:  725764018ce ssh signing: parse ssh-keygen output and verify signatures\n  6:  eb677b1b6a8 !  6:  18a26ca49e7 ssh signing: add test prereqs\n     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '\n      +\ttest $? = 0 || exit 1;\n      +\tmkdir -p \"${GNUPGHOME}\" &&\n      +\tchmod 0700 \"${GNUPGHOME}\" &&\n     -+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n     -+\tssh-keygen -t rsa -b 2048 -N \"\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n     -+\tssh-keygen -t ed25519 -N \"super_secret\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n     -+\tfind \"${GNUPGHOME}\" -name *ssh_signing_key.pub -exec cat {} \\; | awk \"{print \\\"\\\\\\\"principal with number \\\" NR \\\"\\\\\\\" \\\" \\$0}\" > \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     ++\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n     ++\techo \"\\\"principal with number 1\\\" $(cat \"${GNUPGHOME}/ed25519_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     ++\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n     ++\techo \"\\\"principal with number 2\\\" $(cat \"${GNUPGHOME}/rsa_2048_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     ++\tssh-keygen -t ed25519 -N \"super_secret\" -C \"git ed25519 encrypted key\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n     ++\techo \"\\\"principal with number 3\\\" $(cat \"${GNUPGHOME}/protected_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n      +\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n      +\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n      +'\n  7:  c877951df23 =  7:  01da9a07934 ssh signing: duplicate t7510 tests for commits\n  8:  60265e8c399 =  8:  d9707443f5c ssh signing: add more tests for logs, tags & push certs\n  9:  f758ce0ade4 =  9:  275af516eba ssh signing: add documentation\n\n-- \ngitgitgadget\n"},{"id":"431420","messageId":"7c8502c65b833e7e563a833b592f6932421b1056.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:41Z","receivedAt":"2021-07-28T19:36:56Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nOpenssh v8.2p1 added some new options to ssh-keygen for signature\ncreation and verification. These allow us to use ssh keys for git\nsignatures easily.\n\nIn our corporate environment we use PIV x509 Certs on Yubikeys for email\nsigning/encryption and ssh keys which I think is quite common\n(at least for the email part). This way we can establish the correct\ntrust for the SSH Keys without setting up a separate GPG Infrastructure\n(which is still quite painful for users) or implementing x509 signing\nsupport for git (which lacks good forwarding mechanisms).\nUsing ssh agent forwarding makes this feature easily usable in todays\ndevelopment environments where code is often checked out in remote VMs / containers.\nIn such a setup the keyring & revocationKeyring can be centrally\ngenerated from the x509 CA information and distributed to the users.\n\nTo be able to implement new signing formats this commit:\n - makes the sigc structure more generic by renaming \"gpg_output\" to\n   \"output\"\n - introduces function pointers in the gpg_format structure to call\n   format specific signing and verification functions\n - moves format detection from verify_signed_buffer into the check_signature\n   api function and calls the format specific verify\n - renames and wraps sign_buffer to handle format specific signing logic\n   as well\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n fmt-merge-msg.c |   6 +--\n gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------\n gpg-interface.h |   2 +-\n log-tree.c      |   8 ++--\n pretty.c        |   4 +-\n 5 files changed, 74 insertions(+), 50 deletions(-)\n\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 0f66818e0f8..fb300bb4b67 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -526,11 +526,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tbuf = payload.buf;\n \t\t\tlen = payload.len;\n \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n-\t\t\t\t\t sig.len, &sigc) &&\n-\t\t\t\t!sigc.gpg_output)\n+\t\t\t\t\t    sig.len, &sigc) &&\n+\t\t\t    !sigc.output)\n \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n \t\t\telse\n-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n \t\t}\n \t\tsignature_check_clear(&sigc);\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..31cf4ba3938 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -15,6 +15,12 @@ struct gpg_format {\n \tconst char *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n+\tint (*verify_signed_buffer)(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n+\n static struct gpg_format gpg_format[] = {\n-\t{ .name = \"openpgp\", .program = \"gpg\",\n-\t  .verify_args = openpgp_verify_args,\n-\t  .sigs = openpgp_sigs\n+\t{\n+\t\t.name = \"openpgp\",\n+\t\t.program = \"gpg\",\n+\t\t.verify_args = openpgp_verify_args,\n+\t\t.sigs = openpgp_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n-\t{ .name = \"x509\", .program = \"gpgsm\",\n-\t  .verify_args = x509_verify_args,\n-\t  .sigs = x509_sigs\n+\t{\n+\t\t.name = \"x509\",\n+\t\t.program = \"gpgsm\",\n+\t\t.verify_args = x509_verify_args,\n+\t\t.sigs = x509_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n };\n \n@@ -72,7 +93,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\n-\tFREE_AND_NULL(sigc->gpg_output);\n+\tFREE_AND_NULL(sigc->output);\n \tFREE_AND_NULL(sigc->gpg_status);\n \tFREE_AND_NULL(sigc->signer);\n \tFREE_AND_NULL(sigc->key);\n@@ -257,16 +278,16 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n-static int verify_signed_buffer(const char *payload, size_t payload_size,\n-\t\t\t\tconst char *signature, size_t signature_size,\n-\t\t\t\tstruct strbuf *gpg_output,\n-\t\t\t\tstruct strbuf *gpg_status)\n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n-\tstruct gpg_format *fmt;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf gpg_stdout = STRBUF_INIT;\n+\tstruct strbuf gpg_stderr = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -279,10 +300,6 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\treturn -1;\n \t}\n \n-\tfmt = get_format_by_sig(signature);\n-\tif (!fmt)\n-\t\tBUG(\"bad signature '%s'\", signature);\n-\n \tstrvec_push(&gpg.args, fmt->program);\n \tstrvec_pushv(&gpg.args, fmt->verify_args);\n \tstrvec_pushl(&gpg.args,\n@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\t     \"--verify\", temp->filename.buf, \"-\",\n \t\t     NULL);\n \n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n-\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n+\tret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,\n+\t\t\t   &gpg_stderr, 0);\n \tsigchain_pop(SIGPIPE);\n \n \tdelete_tempfile(&temp);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n-\tstrbuf_release(&buf); /* no matter it was used or not */\n+\tret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tsigc->output = strbuf_detach(&gpg_stderr, NULL);\n+\tsigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);\n+\n+\tparse_gpg_output(sigc);\n+\n+\tstrbuf_release(&gpg_stdout);\n+\tstrbuf_release(&gpg_stderr);\n \n \treturn ret;\n }\n@@ -309,35 +330,32 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n-\tstruct strbuf gpg_output = STRBUF_INIT;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct gpg_format *fmt;\n \tint status;\n \n \tsigc->result = 'N';\n \tsigc->trust_level = -1;\n \n-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n-\t\t\t\t      &gpg_output, &gpg_status);\n-\tif (status && !gpg_output.len)\n-\t\tgoto out;\n-\tsigc->payload = xmemdupz(payload, plen);\n-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tfmt = get_format_by_sig(signature);\n+\tif (!fmt)\n+\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n+\n+\tstatus = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,\n+\t\t\t\t\t   slen);\n+\n+\tif (status && !sigc->output)\n+\t\treturn !!status;\n+\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n- out:\n-\tstrbuf_release(&gpg_status);\n-\tstrbuf_release(&gpg_output);\n-\n \treturn !!status;\n }\n \n void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n {\n-\tconst char *output = flags & GPG_VERIFY_RAW ?\n-\t\tsigc->gpg_status : sigc->gpg_output;\n+\tconst char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :\n+\t\t\t\t\t\t\t    sigc->output;\n \n \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n \t\tfputs(sigc->payload, stdout);\n@@ -441,6 +459,12 @@ const char *get_signing_key(void)\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n+{\n+\treturn use_format->sign_buffer(buffer, signature, signing_key);\n+}\n+\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t    const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..feac4decf8b 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -17,7 +17,7 @@ enum signature_trust_level {\n \n struct signature_check {\n \tchar *payload;\n-\tchar *gpg_output;\n+\tchar *output;\n \tchar *gpg_status;\n \n \t/*\ndiff --git a/log-tree.c b/log-tree.c\nindex 7b823786c2c..20af9bd1c82 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit)\n \n \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n \t\t\t\t signature.len, &sigc);\n-\tif (status && !sigc.gpg_output)\n+\tif (status && !sigc.output)\n \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n \telse\n-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n+\t\tshow_sig_lines(opt, status, sigc.output);\n \tsignature_check_clear(&sigc);\n \n  out:\n@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,\n \t\t/* could have a good signature */\n \t\tstatus = check_signature(payload.buf, payload.len,\n \t\t\t\t\t signature.buf, signature.len, &sigc);\n-\t\tif (sigc.gpg_output)\n-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n+\t\tif (sigc.output)\n+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n \t\telse\n \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n \t\tsignature_check_clear(&sigc);\ndiff --git a/pretty.c b/pretty.c\nindex b1ecd039cef..daa71394efd 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n \t\tswitch (placeholder[1]) {\n \t\tcase 'G':\n-\t\t\tif (c->signature_check.gpg_output)\n-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n+\t\t\tif (c->signature_check.output)\n+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n \t\t\tbreak;\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n-- \ngitgitgadget\n\n"},{"id":"431421","messageId":"f05bab16096c080891ee8f7e179eecce7f32e839.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:42Z","receivedAt":"2021-07-28T19:36:58Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nimplements the actual sign_buffer_ssh operation and move some shared\ncleanup code into a strbuf function\n\nSet gpg.format = ssh and user.signingkey to either a ssh public key\nstring (like from an authorized_keys file), or a ssh key file.\nIf the key file or the config value itself contains only a public key\nthen the private key needs to be available via ssh-agent.\n\ngpg.ssh.program can be set to an alternative location of ssh-keygen.\nA somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\nthis feature. Since only ssh-keygen is needed it can this way be\ninstalled seperately without upgrading your system openssh packages.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---\n 1 file changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 31cf4ba3938..c131977b347 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = { NULL };\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n \n static struct gpg_format gpg_format[] = {\n \t{\n@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t},\n+\t{\n+\t\t.name = \"ssh\",\n+\t\t.program = \"ssh-keygen\",\n+\t\t.verify_args = ssh_verify_args,\n+\t\t.sigs = ssh_sigs,\n+\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.sign_buffer = sign_buffer_ssh\n+\t},\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+/*\n+ * Strip CR from the line endings, in case we are on Windows.\n+ * NEEDSWORK: make it trim only CRs before LFs and rename\n+ */\n+static void remove_cr_after(struct strbuf *buffer, size_t offset)\n+{\n+\tsize_t i, j;\n+\n+\tfor (i = j = offset; i < buffer->len; i++) {\n+\t\tif (buffer->buf[i] != '\\r') {\n+\t\t\tif (i != j)\n+\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n+\t\t\tj++;\n+\t\t}\n+\t}\n+\tstrbuf_setlen(buffer, j);\n+}\n+\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t    const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\n-\tsize_t i, j, bottom;\n+\tsize_t bottom;\n \tstruct strbuf gpg_status = STRBUF_INIT;\n \n \tstrvec_pushl(&gpg.args,\n@@ -494,13 +531,97 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\treturn error(_(\"gpg failed to sign the data\"));\n \n \t/* Strip CR from the line endings, in case we are on Windows. */\n-\tfor (i = j = bottom; i < signature->len; i++)\n-\t\tif (signature->buf[i] != '\\r') {\n-\t\t\tif (i != j)\n-\t\t\t\tsignature->buf[j] = signature->buf[i];\n-\t\t\tj++;\n-\t\t}\n-\tstrbuf_setlen(signature, j);\n+\tremove_cr_after(signature, bottom);\n \n \treturn 0;\n }\n+\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key)\n+{\n+\tstruct child_process signer = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tsize_t bottom, keylen;\n+\tstruct strbuf signer_stderr = STRBUF_INIT;\n+\tstruct tempfile *key_file = NULL, *buffer_file = NULL;\n+\tchar *ssh_signing_key_file = NULL;\n+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n+\n+\tif (!signing_key || signing_key[0] == '\\0')\n+\t\treturn error(\n+\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n+\n+\tif (starts_with(signing_key, \"ssh-\")) {\n+\t\t/* A literal ssh key */\n+\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\tif (!key_file)\n+\t\t\treturn error_errno(\n+\t\t\t\t_(\"could not create temporary file\"));\n+\t\tkeylen = strlen(signing_key);\n+\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n+\t\t    close_tempfile_gently(key_file) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n+\t\t\t\t    key_file->filename.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tssh_signing_key_file = key_file->filename.buf;\n+\t} else {\n+\t\t/* We assume a file */\n+\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n+\tif (!buffer_file) {\n+\t\terror_errno(_(\"could not create temporary file\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tstrvec_pushl(&signer.args, use_format->program,\n+\t\t     \"-Y\", \"sign\",\n+\t\t     \"-n\", \"git\",\n+\t\t     \"-f\", ssh_signing_key_file,\n+\t\t     buffer_file->filename.buf,\n+\t\t     NULL);\n+\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n+\tsigchain_pop(SIGPIPE);\n+\n+\tif (ret) {\n+\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n+\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n+\n+\t\terror(\"%s\", signer_stderr.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tbottom = signature->len;\n+\n+\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n+\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n+\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {\n+\t\terror_errno(\n+\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n+\t\t\tssh_signature_filename.buf);\n+\t}\n+\tunlink_or_warn(ssh_signature_filename.buf);\n+\n+\t/* Strip CR from the line endings, in case we are on Windows. */\n+\tremove_cr_after(signature, bottom);\n+\n+out:\n+\tif (key_file)\n+\t\tdelete_tempfile(&key_file);\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&signer_stderr);\n+\tstrbuf_release(&ssh_signature_filename);\n+\treturn ret;\n+}\n-- \ngitgitgadget\n\n"},{"id":"431422","messageId":"071e6173d8e418349d94fea97624e8cee9f1dde5.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:43Z","receivedAt":"2021-07-28T19:36:59Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nif user.signingkey is not set and a ssh signature is requested we call\nssh-add -L and use the first key we get\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 26 +++++++++++++++++++++++++-\n 1 file changed, 25 insertions(+), 1 deletion(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex c131977b347..3afacb48900 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+/* Returns the first public key from an ssh-agent to use for signing */\n+static char *get_default_ssh_signing_key(void)\n+{\n+\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf key_stdout = STRBUF_INIT;\n+\tstruct strbuf **keys;\n+\n+\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n+\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n+\tif (!ret) {\n+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n+\t\tif (keys[0])\n+\t\t\treturn strbuf_detach(keys[0], NULL);\n+\t}\n+\n+\tstrbuf_release(&key_stdout);\n+\treturn \"\";\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_default_ssh_signing_key();\n+\t} else {\n+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n+\t}\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n-- \ngitgitgadget\n\n"},{"id":"431423","messageId":"725764018ceb5bcecc748cc5169d4305ea9d7d23.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:45Z","receivedAt":"2021-07-28T19:37:00Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nto verify a ssh signature we first call ssh-keygen -Y find-principal to\nlook up the signing principal by their public key from the\nallowedSignersFile. If the key is found then we do a verify. Otherwise\nwe only validate the signature but can not verify the signers identity.\n\nVerification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\nSIGNERS\") which contains valid public keys and a principal (usually\nuser@domain). Depending on the environment this file can be managed by\nthe individual developer or for example generated by the central\nrepository server from known ssh keys with push access. If the\nrepository only allows signed commits / pushes then the file can even be\nstored inside it.\n\nTo revoke a key put the public key without the principal prefix into\ngpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n\"KEY REVOCATION LISTS\"). The same considerations about who to trust for\nverification as with the allowedSignersFile apply.\n\nUsing SSH CA Keys with these files is also possible. Add\n\"cert-authority\" as key option between the principal and the key to mark\nit as a CA and all keys signed by it as valid for this CA.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n builtin/receive-pack.c |   2 +\n gpg-interface.c        | 179 ++++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 180 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513ac..62b11c5f3a4 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n {\n \tint status = parse_hide_refs_config(var, value, \"receive\");\n \n+\tgit_gpg_config(var, value, NULL);\n+\n \tif (status)\n \t\treturn status;\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex ec48a37b6cc..703225c3cd3 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -3,11 +3,13 @@\n #include \"config.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n+#include \"dir.h\"\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n \n static char *configured_signing_key;\n+static const char *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -51,6 +53,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n@@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.program = \"ssh-keygen\",\n \t\t.verify_args = ssh_verify_args,\n \t\t.sigs = ssh_sigs,\n-\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n \t\t.sign_buffer = sign_buffer_ssh\n \t},\n };\n@@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \treturn ret;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tconst char *line, *principal, *search;\n+\n+\t/*\n+\t * ssh-keysign output should be:\n+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n+\t * or for valid but unknown keys:\n+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n+\t */\n+\tsigc->result = 'B';\n+\tsigc->trust_level = TRUST_NEVER;\n+\n+\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n+\n+\tif (skip_prefix(line, \"Good \\\"git\\\" signature for \", &line)) {\n+\t\t/* Valid signature and known principal */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_FULLY;\n+\n+\t\t/* Search for the last \"with\" to get the full principal */\n+\t\tprincipal = line;\n+\t\tdo {\n+\t\t\tsearch = strstr(line, \" with \");\n+\t\t\tif (search)\n+\t\t\t\tline = search + 1;\n+\t\t} while (search != NULL);\n+\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n+\t\t/* Valid signature, but key unknown */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_UNDEFINED;\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t}\n+}\n+\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tstruct tempfile *buffer_file;\n+\tint ret = -1;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n+\n+\tif (!ssh_allowed_signers) {\n+\t\terror(_(\"gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification\"));\n+\t\treturn -1;\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n+\tif (!buffer_file)\n+\t\treturn error_errno(_(\"could not create temporary file\"));\n+\tif (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tdelete_tempfile(&buffer_file);\n+\t\treturn -1;\n+\t}\n+\n+\t/* Find the principal from the signers */\n+\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t     \"-Y\", \"find-principals\",\n+\t\t     \"-f\", ssh_allowed_signers,\n+\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t     NULL);\n+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n+\t\t\t   &ssh_keygen_err, 0);\n+\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n+\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n+\t\tgoto out;\n+\t}\n+\tif (ret || !ssh_keygen_out.len) {\n+\t\t/* We did not find a matching principal in the allowedSigners - Check\n+\t\t * without validation */\n+\t\tchild_process_init(&ssh_keygen);\n+\t\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t\t     \"-Y\", \"check-novalidate\",\n+\t\t\t     \"-n\", \"git\",\n+\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t     NULL);\n+\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t} else {\n+\t\t/* Check every principal we found (one per line) */\n+\t\tfor (line = ssh_keygen_out.buf; *line;\n+\t\t     line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n+\n+\t\t\ttrust_size = strcspn(line, \"\\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n+\n+\t\t\tchild_process_init(&ssh_keygen);\n+\t\t\tstrbuf_release(&ssh_keygen_out);\n+\t\t\tstrbuf_release(&ssh_keygen_err);\n+\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n+\t\t\t/* We found principals - Try with each until we find a\n+\t\t\t * match */\n+\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\",\n+\t\t\t\t     \"-n\", \"git\",\n+\t\t\t\t     \"-f\", ssh_allowed_signers,\n+\t\t\t\t     \"-I\", principal,\n+\t\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t\t     NULL);\n+\n+\t\t\tif (ssh_revocation_file) {\n+\t\t\t\tif (file_exists(ssh_revocation_file)) {\n+\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\",\n+\t\t\t\t\t\t     ssh_revocation_file, NULL);\n+\t\t\t\t} else {\n+\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"),\n+\t\t\t\t\t\tssh_revocation_file);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n+\n+\t\t\tFREE_AND_NULL(principal);\n+\n+\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n+\t\t\tif (ret == 0)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n+\tsigc->gpg_status = xstrdup(sigc->output);\n+\n+\tparse_ssh_output(sigc);\n+\n+out:\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&ssh_keygen_out);\n+\tstrbuf_release(&ssh_keygen_err);\n+\n+\treturn ret;\n+}\n+\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n@@ -453,6 +618,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationFile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \n-- \ngitgitgadget\n\n"},{"id":"431424","messageId":"18a26ca49e7a9b0046559ac8d5c62c99ea7262ae.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 6/9] ssh signing: add test prereqs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:46Z","receivedAt":"2021-07-28T19:37:02Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\ngenerate some ssh keys and a allowedSignersFile for testing\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/lib-gpg.sh | 29 +++++++++++++++++++++++++++++\n 1 file changed, 29 insertions(+)\n\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\nindex 9fc5241228e..600c8d1a026 100644\n--- a/t/lib-gpg.sh\n+++ b/t/lib-gpg.sh\n@@ -87,6 +87,35 @@ test_lazy_prereq RFC1991 '\n \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n '\n \n+test_lazy_prereq GPGSSH '\n+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n+\ttest $? != 127 || exit 1\n+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n+\ttest $? = 0 || exit 1;\n+\tmkdir -p \"${GNUPGHOME}\" &&\n+\tchmod 0700 \"${GNUPGHOME}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n+\techo \"\\\"principal with number 1\\\" $(cat \"${GNUPGHOME}/ed25519_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n+\techo \"\\\"principal with number 2\\\" $(cat \"${GNUPGHOME}/rsa_2048_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tssh-keygen -t ed25519 -N \"super_secret\" -C \"git ed25519 encrypted key\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n+\techo \"\\\"principal with number 3\\\" $(cat \"${GNUPGHOME}/protected_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n+'\n+\n+SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n+SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n+SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n+SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n+SIGNING_KEY_PASSPHRASE=\"super_secret\"\n+SIGNING_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n+\n+GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n+GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n+KEY_NOT_TRUSTED=\"No principal matched\"\n+BAD_SIGNATURE=\"Signature verification failed\"\n+\n sanitize_pgp() {\n \tperl -ne '\n \t\t/^-----END PGP/ and $in_pgp = 0;\n-- \ngitgitgadget\n\n"},{"id":"431425","messageId":"7d1d131ff5b43559c8a750ebdfd6faaba93c1ad1.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 4/9] ssh signing: provide a textual representation of the signing key","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:44Z","receivedAt":"2021-07-28T19:37:03Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nfor ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\nin push certs and textual output we prefer the ssh fingerprint instead.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++\n gpg-interface.h |  6 ++++++\n send-pack.c     |  8 ++++----\n 3 files changed, 56 insertions(+), 4 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 3afacb48900..ec48a37b6cc 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+static char *get_ssh_key_fingerprint(const char *signing_key)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n+\tstruct strbuf **fingerprint;\n+\n+\t/*\n+\t * With SSH Signing this can contain a filename or a public key\n+\t * For textual representation we usually want a fingerprint\n+\t */\n+\tif (istarts_with(signing_key, \"ssh-\")) {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n+\t\tret = pipe_command(&ssh_keygen, signing_key,\n+\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t} else {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\",\n+\t\t\t     configured_signing_key, NULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t}\n+\n+\tif (!!ret)\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n+\tif (!fingerprint[1])\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\treturn strbuf_detach(fingerprint[1], NULL);\n+}\n+\n /* Returns the first public key from an ssh-agent to use for signing */\n static char *get_default_ssh_signing_key(void)\n {\n@@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)\n \treturn \"\";\n }\n \n+/* Returns a textual but unique representation ot the signing key */\n+const char *get_signing_key_id(void)\n+{\n+\tif (!strcmp(use_format->name, \"ssh\")) {\n+\t\treturn get_ssh_key_fingerprint(get_signing_key());\n+\t} else {\n+\t\t/* GPG/GPGSM only store a key id on this variable */\n+\t\treturn get_signing_key();\n+\t}\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex feac4decf8b..beefacbb1e9 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+\n+/*\n+ * Returns a textual unique representation of the signing key in use\n+ * Either a GPG KeyID or a SSH Key Fingerprint\n+ */\n+const char *get_signing_key_id(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\ndiff --git a/send-pack.c b/send-pack.c\nindex 5a79e0e7110..50cca7e439b 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n {\n \tconst struct ref *ref;\n \tstruct string_list_item *item;\n-\tchar *signing_key = xstrdup(get_signing_key());\n+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n \tconst char *cp, *np;\n \tstruct strbuf cert = STRBUF_INIT;\n \tint update_seen = 0;\n \n \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n \tdatestamp(&cert);\n \tstrbuf_addch(&cert, '\\n');\n \tif (args->url && *args->url) {\n@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, get_signing_key()))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tpacket_buf_write(req_buf, \"push-cert-end\\n\");\n \n free_return:\n-\tfree(signing_key);\n+\tfree(signing_key_id);\n \tstrbuf_release(&cert);\n \treturn update_seen;\n }\n-- \ngitgitgadget\n\n"},{"id":"431426","messageId":"d9707443f5cdd77688cc41df0b15780d9c787eb7.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 8/9] ssh signing: add more tests for logs, tags & push certs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:48Z","receivedAt":"2021-07-28T19:37:04Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t4202-log.sh                   |  23 +++++\n t/t5534-push-signed.sh           | 101 +++++++++++++++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++\n 3 files changed, 285 insertions(+)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 39e746fbcbe..afd7f2516ee 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' '\n \tgit commit -S -m signed_commit\n '\n \n+test_expect_success GPGSSH 'setup sshkey signed branch' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n+\tgit checkout -b signed-ssh main &&\n+\techo foo >foo &&\n+\tgit add foo &&\n+\tgit commit -S -m signed_commit\n+'\n+\n test_expect_success GPGSM 'log x509 fingerprint' '\n \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPGSSH 'log ssh key fingerprint' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature' '\n \tgit log --graph --show-signature -n1 signed >actual &&\n \tgrep \"^| gpg: Signature made\" actual &&\n@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' '\n \tgrep \"^| gpgsm: Good signature\" actual\n '\n \n+test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature for merged tag' '\n \ttest_when_finished \"git reset --hard && git checkout main\" &&\n \tgit checkout -b plain main &&\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex bba768f5ded..d590249b995 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n \t# First, invoke receive-pack with dummy input to obtain its preamble.\n \tprepare_dst &&\n@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n+\ttest_config gpg.format ssh &&\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config user.email hasnokey@nowhere.com &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"\" &&\n+\t(\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git push --signed dst noop ff +noff\n+\t) &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'failed atomic push does not execute GPG' '\n \tprepare_dst &&\n \tgit -C dst config receive.certnonceseed sekrit &&\ndiff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh\nnew file mode 100755\nindex 00000000000..05bf520a332\n--- /dev/null\n+++ b/t/t7031-verify-tag-signed-ssh.sh\n@@ -0,0 +1,161 @@\n+#!/bin/sh\n+\n+test_description='signed tag tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed tags ssh' '\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -m initial &&\n+\tgit tag -s -m initial initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -m second &&\n+\tgit tag -s -m second second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag -s -m merge merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag -s -m fourth fourth-signed &&\n+\n+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n+\tgit tag fifth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag -a -m sixth sixth-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n+\tgit tag -m seventh -s seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth &&\n+\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify and show ssh signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'detect fudged ssh signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file tag seventh-signed >raw &&\n+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit verify-tag --raw sixth-signed 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\techo sixth-signed OK\n+'\n+\n+test_expect_success GPGSSH 'verify multiple tags ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttags=\"seventh-signed sixth-signed\" &&\n+\tfor i in $tags\n+\tdo\n+\t\tgit verify-tag -v --raw $i || return 1\n+\tdone >expect.stdout 2>expect.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n+\tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n+\ttest_cmp expect.stdout actual.stdout &&\n+\ttest_cmp expect.stderr actual.stderr\n+'\n+\n+test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\ttagname : fourth-signed\n+\tEOF\n+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently - ssh' '\n+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n+\ttest_must_be_empty actual-forged\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"431427","messageId":"275af516ebadebf0af6555f27508bdff98a35b94.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 9/9] ssh signing: add documentation","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:49Z","receivedAt":"2021-07-28T19:37:05Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  | 39 +++++++++++++++++++++++++++++++++--\n Documentation/config/user.txt |  6 ++++++\n 2 files changed, 43 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..dc790512e86 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\n@@ -33,3 +33,38 @@ gpg.minTrustLevel::\n * `marginal`\n * `fully`\n * `ultimate`\n+\n+gpg.ssh.allowedSignersFile::\n+\tA file containing ssh public keys which you are willing to trust.\n+\tThe file consists of one or more lines of principals followed by an ssh\n+\tpublic key.\n+\te.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...\n+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n+\tThe principal is only used to identify the key and is available when\n+\tverifying a signature.\n++\n+SSH has no concept of trust levels like gpg does. To be able to differentiate\n+between valid signatures and trusted signatures the trust level of a signature\n+verification is set to `fully` when the public key is present in the allowedSignersFile.\n+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.\n+Otherwise valid but untrusted signatures will still verify but show no principal\n+name of the signer.\n++\n+This file can be set to a location outside of the repository and every developer\n+maintains their own trust store. A central repository server could generate this\n+file automatically from ssh keys with push access to verify the code against.\n+In a corporate setting this file is probably generated at a global location\n+from automation that already handles developer ssh keys.\n++\n+A repository that only allows signed commits can store the file\n+in the repository itself using a path relative to the top-level of the working tree.\n+This way only committers with an already valid key can add or change keys in the keyring.\n++\n+Using a SSH CA key with the cert-authority option\n+(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n+\n+gpg.ssh.revocationFile::\n+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n+\tSee ssh-keygen(1) for details.\n+\tIf a public key is found in this file then it will always be treated\n+\tas having trust level \"never\" and signatures will show as invalid.\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..b3c2f2c541e 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,9 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n+\tcorresponds to the private key used for signing. The private key\n+\tneeds to be available via ssh-agent. Alternatively it can be set to\n+\ta file containing a private key directly. If not set git will call\n+\t\"ssh-add -L\" and try to use the first key available.\n-- \ngitgitgadget\n"},{"id":"431428","messageId":"01da9a079348e965b00e9ff6cea75dbc74028123.1627501009.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v6 7/9] ssh signing: duplicate t7510 tests for commits","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-07-28T19:36:47Z","receivedAt":"2021-07-28T19:37:05Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++\n 1 file changed, 398 insertions(+)\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\ndiff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh\nnew file mode 100755\nindex 00000000000..e2c48f69e6d\n--- /dev/null\n+++ b/t/t7528-signed-commit-ssh.sh\n@@ -0,0 +1,398 @@\n+#!/bin/sh\n+\n+test_description='ssh signed commit tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed commits' '\n+\ttest_oid_cache <<-\\EOF &&\n+\theader sha1:gpgsig\n+\theader sha256:gpgsig-sha256\n+\tEOF\n+\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -S -m initial &&\n+\tgit tag initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -S -m second &&\n+\tgit tag second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n+\tgit tag fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag fourth-signed &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n+\tgit tag fifth-signed &&\n+\n+\tgit config commit.gpgsign false &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag sixth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n+\tgit tag seventh-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n+\tgit tag seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${SIGNING_KEY_UNTRUSTED}\" &&\n+\tgit tag eighth-signed-alt &&\n+\n+\t# commit.gpgsign is still on but this must not be signed\n+\techo 9 | git commit-tree HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag ninth-unsigned $(cat oid) &&\n+\t# explicit -S of course must sign.\n+\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag tenth-signed $(cat oid) &&\n+\n+\t# --gpg-sign[=<key-id>] must sign.\n+\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag eleventh-signed $(cat oid) &&\n+\techo 12 | git commit-tree --gpg-sign=\"${SIGNING_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag twelfth-signed-alt $(cat oid)\n+'\n+\n+test_expect_success GPGSSH 'verify and show signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed \\\n+\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n+\t\t\televenth-signed\n+\t\tdo\n+\t\t\tgit verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n+\t\t\tseventh-unsigned ninth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n+\t\tdo\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit verify-commit eighth-signed-alt 2>actual &&\n+\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\tgrep \"${KEY_NOT_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n+\tgit cat-file commit fourth-signed >output &&\n+\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n+'\n+\n+test_expect_success GPGSSH 'show signed commit with signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit show -s initial >commit &&\n+\tgit show -s --show-signature initial >show &&\n+\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n+\tgit cat-file commit initial >cat &&\n+\tgrep -v -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n+\tgrep -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n+\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n+\ttest_cmp show.commit commit &&\n+\ttest_cmp show.gpg verify.2 &&\n+\ttest_cmp cat.commit verify.1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t commit forged1 >forged1.commit &&\n+\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature with NUL' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tcat raw >forged2 &&\n+\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n+\tgit hash-object -w -t commit forged2 >forged2.commit &&\n+\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n+\tgrep \"${BAD_SIGNATURE}\" actual2 &&\n+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual2\n+'\n+\n+test_expect_success GPGSSH 'amending already signed commit' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tgit checkout fourth-signed^0 &&\n+\tgit commit --amend -S --no-edit &&\n+\tgit verify-commit HEAD &&\n+\tgit show -s --show-signature HEAD >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${BAD_SIGNATURE}\" actual\n+'\n+\n+test_expect_success GPGSSH 'show good signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tG\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show bad signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\tB\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tU\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tundefined\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tfully\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show lack of signature with custom format' '\n+\tcat >expect <<-\\EOF &&\n+\tN\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n+\ttest_config log.showsignature true &&\n+\tgit show initial >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'check config gpg.format values' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n+\ttest_config gpg.format ssh &&\n+\tgit commit -S --amend -m \"success\" &&\n+\ttest_config gpg.format OpEnPgP &&\n+\ttest_must_fail git commit -S --amend -m \"fail\"\n+'\n+\n+test_expect_failure GPGSSH 'detect fudged commit with double signature (TODO)' '\n+\tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n+\tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n+\t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n+\tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n+\tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n+\tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n+\t\tdouble-combined.asc > double-gpgsig &&\n+\tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n+\tgit hash-object -w -t commit double-commit >double-commit.commit &&\n+\ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n+\tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n+\tgrep \"BAD signature from\" double-actual &&\n+\tgrep \"Good signature from\" double-actual\n+'\n+\n+test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '\n+\tcat >expect <<-\\EOF &&\n+\tE\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '\n+\tgit init multiply-signed &&\n+\tcd multiply-signed &&\n+\ttest_commit first &&\n+\techo 1 >second &&\n+\tgit add second &&\n+\ttree=$(git write-tree) &&\n+\tparent=$(git rev-parse HEAD^{commit}) &&\n+\tgit commit --gpg-sign -m second &&\n+\tgit cat-file commit HEAD &&\n+\t# Avoid trailing whitespace.\n+\tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n+\tQtree $tree\n+\tQparent $parent\n+\tQauthor A U Thor <author@example.com> 1112912653 -0700\n+\tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n+\tQgpgsig -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n+\tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n+\tQ =tQ0N\n+\tQ -----END PGP SIGNATURE-----\n+\tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n+\tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n+\tQ =pIwP\n+\tQ -----END PGP SIGNATURE-----\n+\tQ\n+\tQsecond\n+\tEOF\n+\thead=$(git hash-object -t commit -w commit) &&\n+\tgit reset --hard $head &&\n+\tgit verify-commit $head 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"431437","messageId":"xmqqim0u86w2.fsf@gitster.g","threadId":"56054","inReplyTo":"071e6173d8e418349d94fea97624e8cee9f1dde5.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-28T21:29:01Z","receivedAt":"2021-07-28T21:29:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> if user.signingkey is not set and a ssh signature is requested we call\n> ssh-add -L and use the first key we get\n>\n> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n> ---\n>  gpg-interface.c | 26 +++++++++++++++++++++++++-\n>  1 file changed, 25 insertions(+), 1 deletion(-)\n\nI would have expected that this also would become a method call into\n*use_format object (instead of dispatching on use_format->name), but\nlet's not go overboard.  I think this is good enough for now.\n\n\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index c131977b347..3afacb48900 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n>  \treturn 0;\n>  }\n>  \n> +/* Returns the first public key from an ssh-agent to use for signing */\n> +static char *get_default_ssh_signing_key(void)\n> +{\n> +\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tstruct strbuf key_stdout = STRBUF_INIT;\n> +\tstruct strbuf **keys;\n> +\n> +\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n> +\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n> +\tif (!ret) {\n> +\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n> +\t\tif (keys[0])\n> +\t\t\treturn strbuf_detach(keys[0], NULL);\n> +\t}\n> +\n> +\tstrbuf_release(&key_stdout);\n> +\treturn \"\";\n> +}\n> +\n>  const char *get_signing_key(void)\n>  {\n>  \tif (configured_signing_key)\n>  \t\treturn configured_signing_key;\n> -\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n> +\tif (!strcmp(use_format->name, \"ssh\")) {\n> +\t\treturn get_default_ssh_signing_key();\n> +\t} else {\n> +\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n> +\t}\n>  }\n>  \n>  int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n"},{"id":"431438","messageId":"xmqqeebi86m7.fsf@gitster.g","threadId":"56054","inReplyTo":"7d1d131ff5b43559c8a750ebdfd6faaba93c1ad1.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 4/9] ssh signing: provide a textual representation of the signing key","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-28T21:34:56Z","receivedAt":"2021-07-28T21:35:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\n> in push certs and textual output we prefer the ssh fingerprint instead.\n\nThese sentences that lack the initial capital letters would look\nunusual and distracting in our \"git log --no-merges\" stream.\n\n> +static char *get_ssh_key_fingerprint(const char *signing_key)\n> +{\n> +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n> +\tstruct strbuf **fingerprint;\n> +\n> +\t/*\n> +\t * With SSH Signing this can contain a filename or a public key\n> +\t * For textual representation we usually want a fingerprint\n> +\t */\n> +\tif (istarts_with(signing_key, \"ssh-\")) {\n> +\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n> +\t\tret = pipe_command(&ssh_keygen, signing_key,\n> +\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n> +\t\t\t\t   NULL, 0);\n> +\t} else {\n> +\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\",\n> +\t\t\t     configured_signing_key, NULL);\n> +\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n> +\t\t\t\t   NULL, 0);\n> +\t}\n> +\n> +\tif (!!ret)\n> +\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n> +\t\t\t  signing_key);\n> +\n> +\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n> +\tif (!fingerprint[1])\n> +\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n> +\t\t\t  signing_key);\n> +\n> +\treturn strbuf_detach(fingerprint[1], NULL);\n> +}\n> +\n>  /* Returns the first public key from an ssh-agent to use for signing */\n>  static char *get_default_ssh_signing_key(void)\n>  {\n> @@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)\n>  \treturn \"\";\n>  }\n>  \n> +/* Returns a textual but unique representation ot the signing key */\n\n\"ot\" -> \"of\".\n\n> +const char *get_signing_key_id(void)\n> +{\n> +\tif (!strcmp(use_format->name, \"ssh\")) {\n> +\t\treturn get_ssh_key_fingerprint(get_signing_key());\n> +\t} else {\n> +\t\t/* GPG/GPGSM only store a key id on this variable */\n> +\t\treturn get_signing_key();\n\nHmph, we could ask gpg key fingerprint if we wanted to, and we\ncannot tell why \"ssh\" side needs a separate \"key\" and \"key_id\"\nwhile \"gpg\" side does not.  Hopefully it will become clear as we\nread on?\n\nAgain, dispatching on use_format->name looked rather unexpected.\n\n> +\t}\n> +}\n> +\n>  const char *get_signing_key(void)\n>  {\n>  \tif (configured_signing_key)\n> diff --git a/gpg-interface.h b/gpg-interface.h\n> index feac4decf8b..beefacbb1e9 100644\n> --- a/gpg-interface.h\n> +++ b/gpg-interface.h\n> @@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n>  int git_gpg_config(const char *, const char *, void *);\n>  void set_signing_key(const char *);\n>  const char *get_signing_key(void);\n> +\n> +/*\n> + * Returns a textual unique representation of the signing key in use\n> + * Either a GPG KeyID or a SSH Key Fingerprint\n> + */\n> +const char *get_signing_key_id(void);\n>  int check_signature(const char *payload, size_t plen,\n>  \t\t    const char *signature, size_t slen,\n>  \t\t    struct signature_check *sigc);\n> diff --git a/send-pack.c b/send-pack.c\n> index 5a79e0e7110..50cca7e439b 100644\n> --- a/send-pack.c\n> +++ b/send-pack.c\n> @@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n>  {\n>  \tconst struct ref *ref;\n>  \tstruct string_list_item *item;\n> -\tchar *signing_key = xstrdup(get_signing_key());\n> +\tchar *signing_key_id = xstrdup(get_signing_key_id());\n>  \tconst char *cp, *np;\n>  \tstruct strbuf cert = STRBUF_INIT;\n>  \tint update_seen = 0;\n>  \n>  \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n> -\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n> +\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n\nAhh...  We do not send GPG fingerprint in push certificate but you\nwant to use the fingerprint when signing with SSH keys, and that is\nwhere the need for signing_key_id comes from?\n\nOK.\n"},{"id":"431439","messageId":"xmqq35ry85or.fsf@gitster.g","threadId":"56054","inReplyTo":"725764018ceb5bcecc748cc5169d4305ea9d7d23.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-28T21:55:00Z","receivedAt":"2021-07-28T21:55:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> to verify a ssh signature we first call ssh-keygen -Y find-principal to\n\n\"to\" -> \"To\".\n\n> look up the signing principal by their public key from the\n> allowedSignersFile. If the key is found then we do a verify. Otherwise\n> we only validate the signature but can not verify the signers identity.\n>\n> Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\n> SIGNERS\") which contains valid public keys and a principal (usually\n> user@domain). Depending on the environment this file can be managed by\n> the individual developer or for example generated by the central\n> repository server from known ssh keys with push access. If the\n> repository only allows signed commits / pushes then the file can even be\n> stored inside it.\n>\n> To revoke a key put the public key without the principal prefix into\n> gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n> \"KEY REVOCATION LISTS\"). The same considerations about who to trust for\n> verification as with the allowedSignersFile apply.\n>\n> Using SSH CA Keys with these files is also possible. Add\n> \"cert-authority\" as key option between the principal and the key to mark\n> it as a CA and all keys signed by it as valid for this CA.\n>\n> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n> ---\n>  builtin/receive-pack.c |   2 +\n>  gpg-interface.c        | 179 ++++++++++++++++++++++++++++++++++++++++-\n>  2 files changed, 180 insertions(+), 1 deletion(-)\n\nA lot of additions to support a new system, all looking quite\nstraight-forward.\n\n> @@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = {\n>  \t\t.program = \"ssh-keygen\",\n>  \t\t.verify_args = ssh_verify_args,\n>  \t\t.sigs = ssh_sigs,\n> -\t\t.verify_signed_buffer = NULL, /* TODO */\n> +\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n>  \t\t.sign_buffer = sign_buffer_ssh\n>  \t},\n>  };\n\nNice.\n\n> @@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n>  \treturn ret;\n>  }\n>  \n> +static void parse_ssh_output(struct signature_check *sigc)\n> +{\n> +\tconst char *line, *principal, *search;\n> +\n> +\t/*\n> +\t * ssh-keysign output should be:\n> +\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n> +\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n\nA bit unfortunate line that is overly long.  These two are not\nmutually exclusive two different choices, but one is a special case\nof the other, no?  How about phrasing it like so instead?\n\n\t/*\n\t * ssh-keysign output should be:\n\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n         *\n\t * or for valid but unknown keys:\n\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n         *\n\t * Note that \"PRINCIPAL\" can contain whitespace, \"RSA\" and\n\t * \"SHA256\" part could be a different token that names of\n\t * the algorithms used, and \"FINGERPRINT\" is a hexadecimal\n         * string.  By finding the last occurence of \" with \", we can\n         * reliably parse out the PRINCIPAL.\n\t */\n\n> +\t * or for valid but unknown keys:\n> +\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n> +\t */\n> +\tsigc->result = 'B';\n> +\tsigc->trust_level = TRUST_NEVER;\n> +\n> +\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n> +\n> +\tif (skip_prefix(line, \"Good \\\"git\\\" signature for \", &line)) {\n> +\t\t/* Valid signature and known principal */\n> +\t\tsigc->result = 'G';\n> +\t\tsigc->trust_level = TRUST_FULLY;\n> +\n> +\t\t/* Search for the last \"with\" to get the full principal */\n> +\t\tprincipal = line;\n> +\t\tdo {\n> +\t\t\tsearch = strstr(line, \" with \");\n> +\t\t\tif (search)\n> +\t\t\t\tline = search + 1;\n> +\t\t} while (search != NULL);\n> +\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n> +\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n\nOK.  This does not care the \"RSA\" part, which is future resistant.\nIt assumes the <algo>:<fingerprint> comes after literal \" key \",\nwhich I think is a reasonable thing to do.\n\nHowever, we never checked if the line has \"key\" in it, so\nstrstr(line, \"key\") + 4 may not be pointing at where this code\nexpects.\n\n> +\t\tsigc->key = xstrdup(sigc->fingerprint);\n> +\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n> +\t\t/* Valid signature, but key unknown */\n> +\t\tsigc->result = 'G';\n> +\t\tsigc->trust_level = TRUST_UNDEFINED;\n> +\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n> +\t\tsigc->key = xstrdup(sigc->fingerprint);\n\nLikewise, I guess.\n\n> +\t}\n> +}\n> +\n> +static int verify_ssh_signed_buffer(struct signature_check *sigc,\n> +\t\t\t\t    struct gpg_format *fmt, const char *payload,\n> +\t\t\t\t    size_t payload_size, const char *signature,\n> +\t\t\t\t    size_t signature_size)\n> +{\n> +\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n> +\tstruct tempfile *buffer_file;\n> +\tint ret = -1;\n> +\tconst char *line;\n> +\tsize_t trust_size;\n> +\tchar *principal;\n> +\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n> +\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n> +\n> +\tif (!ssh_allowed_signers) {\n> +\t\terror(_(\"gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification\"));\n> +\t\treturn -1;\n> +\t}\n> +\n> +\tbuffer_file = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n> +\tif (!buffer_file)\n> +\t\treturn error_errno(_(\"could not create temporary file\"));\n> +\tif (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||\n> +\t    close_tempfile_gently(buffer_file) < 0) {\n> +\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n> +\t\t\t    buffer_file->filename.buf);\n> +\t\tdelete_tempfile(&buffer_file);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\t/* Find the principal from the signers */\n> +\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n> +\t\t     \"-Y\", \"find-principals\",\n> +\t\t     \"-f\", ssh_allowed_signers,\n> +\t\t     \"-s\", buffer_file->filename.buf,\n> +\t\t     NULL);\n> +\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n> +\t\t\t   &ssh_keygen_err, 0);\n> +\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n> +\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n> +\t\tgoto out;\n> +\t}\n> +\tif (ret || !ssh_keygen_out.len) {\n> +\t\t/* We did not find a matching principal in the allowedSigners - Check\n> +\t\t * without validation */\n> +\t\tchild_process_init(&ssh_keygen);\n> +\t\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n> +\t\t\t     \"-Y\", \"check-novalidate\",\n> +\t\t\t     \"-n\", \"git\",\n> +\t\t\t     \"-s\", buffer_file->filename.buf,\n> +\t\t\t     NULL);\n> +\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n> +\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n> +\t} else {\n> +\t\t/* Check every principal we found (one per line) */\n> +\t\tfor (line = ssh_keygen_out.buf; *line;\n> +\t\t     line = strchrnul(line + 1, '\\n')) {\n> +\t\t\twhile (*line == '\\n')\n> +\t\t\t\tline++;\n> +\t\t\tif (!*line)\n> +\t\t\t\tbreak;\n> +\n> +\t\t\ttrust_size = strcspn(line, \"\\n\");\n> +\t\t\tprincipal = xmemdupz(line, trust_size);\n> +\n> +\t\t\tchild_process_init(&ssh_keygen);\n> +\t\t\tstrbuf_release(&ssh_keygen_out);\n> +\t\t\tstrbuf_release(&ssh_keygen_err);\n> +\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n> +\t\t\t/* We found principals - Try with each until we find a\n> +\t\t\t * match */\n\n                        /*\n                         * Do not forget our multi-line comment\n                         * style, please.\n                         */\n\n> +\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\",\n> +\t\t\t\t     \"-n\", \"git\",\n> +\t\t\t\t     \"-f\", ssh_allowed_signers,\n> +\t\t\t\t     \"-I\", principal,\n> +\t\t\t\t     \"-s\", buffer_file->filename.buf,\n> +\t\t\t\t     NULL);\n> +\n> +\t\t\tif (ssh_revocation_file) {\n> +\t\t\t\tif (file_exists(ssh_revocation_file)) {\n> +\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\",\n> +\t\t\t\t\t\t     ssh_revocation_file, NULL);\n> +\t\t\t\t} else {\n> +\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"),\n> +\t\t\t\t\t\tssh_revocation_file);\n> +\t\t\t\t}\n> +\t\t\t}\n> +\n> +\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n> +\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n> +\t\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n> +\t\t\tsigchain_pop(SIGPIPE);\n> +\n> +\t\t\tFREE_AND_NULL(principal);\n> +\n> +\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n\nThis is somewhat unusual construct in our codebase, I suspect.  And\nprobably is even wrong.  Didn't you mean\n\n\t\t\tif (!ret)\n\t\t\t\tret = starts_with(...);\n\ninstead?  Surely, when pipe_command() failed, it is likely that\nssh_keygen_out may not have anything useful, and checking what the\nfirst up-to-four bytes of it contain unconditionally may be cheap\nenough, but the person reading the code would expect you to peek\ninto the result only when you actually got the result, no?\n\n> +\t\t\tif (ret == 0)\n> +\t\t\t\tbreak;\n\nIt's more common to do\n\n\t\t\tif (!ret)\n\t\t\t\tbreak;\n\nin our codebase; in other words, we prefer not to compare with\nliteral 0, like \"if (x == 0)\" or \"if (y != 0)\".\n\nThanks.\n"},{"id":"431440","messageId":"20210728223206.2715554-1-jonathantanmy@google.com","threadId":"56054","inReplyTo":"7c8502c65b833e7e563a833b592f6932421b1056.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2021-07-28T22:32:06Z","receivedAt":"2021-07-28T22:32:14Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"I think this patch set is beyond the \"is this a good idea in general\"\nphase (in particular, I think that being able to sign Git commits by\nusing SSH infrastructure is very useful), so I'll proceed to critiquing\nthe commits in more detail.\n\nFirstly, in commit messages, the left side of the colon is usually the\nname of the subsystem - in this case, \"gpg-interface\".\n\n> To be able to implement new signing formats this commit:\n>  - makes the sigc structure more generic by renaming \"gpg_output\" to\n>    \"output\"\n>  - introduces function pointers in the gpg_format structure to call\n>    format specific signing and verification functions\n>  - moves format detection from verify_signed_buffer into the check_signature\n>    api function and calls the format specific verify\n>  - renames and wraps sign_buffer to handle format specific signing logic\n>    as well\n\nI think that this commit should be further split up - in particular, it\nis hard for reviewers to verify that there is no difference in\nfunctionality before and after this commit. I already spotted one\ndifference - perhaps there are more. For me, splitting the above 4\npoints into 4 commits would be an acceptable split.\n\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 127aecfc2b0..31cf4ba3938 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -15,6 +15,12 @@ struct gpg_format {\n>  \tconst char *program;\n>  \tconst char **verify_args;\n>  \tconst char **sigs;\n> +\tint (*verify_signed_buffer)(struct signature_check *sigc,\n> +\t\t\t\t    struct gpg_format *fmt, const char *payload,\n> +\t\t\t\t    size_t payload_size, const char *signature,\n> +\t\t\t\t    size_t signature_size);\n> +\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t   const char *signing_key);\n>  };\n\n[snip]\n\n>  static struct gpg_format gpg_format[] = {\n> -\t{ .name = \"openpgp\", .program = \"gpg\",\n> -\t  .verify_args = openpgp_verify_args,\n> -\t  .sigs = openpgp_sigs\n> +\t{\n> +\t\t.name = \"openpgp\",\n> +\t\t.program = \"gpg\",\n> +\t\t.verify_args = openpgp_verify_args,\n> +\t\t.sigs = openpgp_sigs,\n> +\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n> +\t\t.sign_buffer = sign_buffer_gpg,\n>  \t},\n> -\t{ .name = \"x509\", .program = \"gpgsm\",\n> -\t  .verify_args = x509_verify_args,\n> -\t  .sigs = x509_sigs\n> +\t{\n> +\t\t.name = \"x509\",\n> +\t\t.program = \"gpgsm\",\n> +\t\t.verify_args = x509_verify_args,\n> +\t\t.sigs = x509_sigs,\n> +\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n> +\t\t.sign_buffer = sign_buffer_gpg,\n>  \t},\n>  };\n\nI think that verify_signed_buffer and sign_buffer should replace\nverify_args and sigs, not be alongside them. In particular, I see from\nlater patches that a new entry will be introduced for SSH, and the\ncorresponding new \"verify\" function does not use verify_args or sigs.\n\n> @@ -279,10 +300,6 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>  \t\treturn -1;\n>  \t}\n>  \n> -\tfmt = get_format_by_sig(signature);\n> -\tif (!fmt)\n> -\t\tBUG(\"bad signature '%s'\", signature);\n\nHere is the difference in functionality that I spotted. Here, lack of\nfmt is fatal...\n\n> @@ -309,35 +330,32 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n>  int check_signature(const char *payload, size_t plen, const char *signature,\n>  \tsize_t slen, struct signature_check *sigc)\n>  {\n> -\tstruct strbuf gpg_output = STRBUF_INIT;\n> -\tstruct strbuf gpg_status = STRBUF_INIT;\n> +\tstruct gpg_format *fmt;\n>  \tint status;\n>  \n>  \tsigc->result = 'N';\n>  \tsigc->trust_level = -1;\n>  \n> -\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n> -\t\t\t\t      &gpg_output, &gpg_status);\n> -\tif (status && !gpg_output.len)\n> -\t\tgoto out;\n> -\tsigc->payload = xmemdupz(payload, plen);\n> -\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n> -\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n> -\tparse_gpg_output(sigc);\n> +\tfmt = get_format_by_sig(signature);\n> +\tif (!fmt)\n> +\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n\n...but here it is not.\n"},{"id":"431441","messageId":"20210728224523.2716969-1-jonathantanmy@google.com","threadId":"56054","inReplyTo":"f05bab16096c080891ee8f7e179eecce7f32e839.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2021-07-28T22:45:23Z","receivedAt":"2021-07-28T22:45:30Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"Keep the commit titles to 50 characters or fewer. E.g.:\n\n  gpg-interface: teach \"ssh\" gpg.format\n\n> implements the actual sign_buffer_ssh operation and move some shared\n> cleanup code into a strbuf function\n\nCapitalization and punctuation.\n\n> Set gpg.format = ssh and user.signingkey to either a ssh public key\n> string (like from an authorized_keys file), or a ssh key file.\n> If the key file or the config value itself contains only a public key\n> then the private key needs to be available via ssh-agent.\n> \n> gpg.ssh.program can be set to an alternative location of ssh-keygen.\n> A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\n> this feature. Since only ssh-keygen is needed it can this way be\n> installed seperately without upgrading your system openssh packages.\n\nI notice that end-user documentation (e.g. about gpg.ssh.program) is in\nits own patch, but could that be added as functionality is being\nimplemented? That makes it easier for reviewers to understand what's\nbeing implemented in each patch.\n\n> @@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n>  \treturn use_format->sign_buffer(buffer, signature, signing_key);\n>  }\n>  \n> +/*\n> + * Strip CR from the line endings, in case we are on Windows.\n> + * NEEDSWORK: make it trim only CRs before LFs and rename\n> + */\n> +static void remove_cr_after(struct strbuf *buffer, size_t offset)\n> +{\n> +\tsize_t i, j;\n> +\n> +\tfor (i = j = offset; i < buffer->len; i++) {\n> +\t\tif (buffer->buf[i] != '\\r') {\n> +\t\t\tif (i != j)\n> +\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n> +\t\t\tj++;\n> +\t\t}\n> +\t}\n> +\tstrbuf_setlen(buffer, j);\n> +}\n\nIn the future, I would prefer refactoring like this to be in its own\npatch. For the moment, this should probably be called \"remove_cr\" (no\n\"after\" as CRs are removed wherever they are in the string).\n\n> +static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t   const char *signing_key)\n> +{\n> +\tstruct child_process signer = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tsize_t bottom, keylen;\n> +\tstruct strbuf signer_stderr = STRBUF_INIT;\n> +\tstruct tempfile *key_file = NULL, *buffer_file = NULL;\n> +\tchar *ssh_signing_key_file = NULL;\n> +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n> +\n> +\tif (!signing_key || signing_key[0] == '\\0')\n> +\t\treturn error(\n> +\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n> +\n> +\tif (starts_with(signing_key, \"ssh-\")) {\n> +\t\t/* A literal ssh key */\n> +\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n> +\t\tif (!key_file)\n> +\t\t\treturn error_errno(\n> +\t\t\t\t_(\"could not create temporary file\"));\n> +\t\tkeylen = strlen(signing_key);\n> +\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n> +\t\t    close_tempfile_gently(key_file) < 0) {\n> +\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n> +\t\t\t\t    key_file->filename.buf);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tssh_signing_key_file = key_file->filename.buf;\n> +\t} else {\n> +\t\t/* We assume a file */\n> +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n> +\t}\n\nA config that has 2 modes of operation is quite error-prone, I think.\nFor example, a user could put a path starting with \"ssh-\" (admittedly\nunlikely since it would usually be an absolute path, but not\nimpossible). And also from an implementation point of view, here the\n\"ssh-\" is case-sensitive, but in a future patch, there is a \"ssh-\" that\nis case-insensitive.\n\nCan this just always take a path?\n\n> +\tif (ret) {\n> +\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n> +\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n> +\n> +\t\terror(\"%s\", signer_stderr.buf);\n> +\t\tgoto out;\n> +\t}\n\nChecking for \"usage:\" seems fragile -  a binary running in a different\nlocale might emit a different string, and legitimate output may somehow\ncontain the string \"usage:\". Is there a different way to detect a\nversion mismatch?\n"},{"id":"431442","messageId":"20210728224832.2717826-1-jonathantanmy@google.com","threadId":"56054","inReplyTo":"071e6173d8e418349d94fea97624e8cee9f1dde5.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2021-07-28T22:48:32Z","receivedAt":"2021-07-28T22:48:36Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"> if user.signingkey is not set and a ssh signature is requested we call\n> ssh-add -L and use the first key we get\n\n[snip]\n\n> +/* Returns the first public key from an ssh-agent to use for signing */\n> +static char *get_default_ssh_signing_key(void)\n> +{\n> +\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tstruct strbuf key_stdout = STRBUF_INIT;\n> +\tstruct strbuf **keys;\n> +\n> +\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n> +\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n> +\tif (!ret) {\n> +\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n> +\t\tif (keys[0])\n> +\t\t\treturn strbuf_detach(keys[0], NULL);\n> +\t}\n> +\n> +\tstrbuf_release(&key_stdout);\n> +\treturn \"\";\n> +}\n\nCould the commit message have a better explanation of why we need this?\n(Also, I would think that the command being run needs to be configurable\ninstead of being just the first \"ssh-add\" in $PATH, and the parsing of\nthe output should be more rigorous. But this is moot if we don't need\nthis feature in the first place.)\n"},{"id":"431445","messageId":"20210728230452.2719333-1-jonathantanmy@google.com","threadId":"56054","inReplyTo":"725764018ceb5bcecc748cc5169d4305ea9d7d23.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2021-07-28T23:04:52Z","receivedAt":"2021-07-28T23:04:56Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"> to verify a ssh signature we first call ssh-keygen -Y find-principal to\n> look up the signing principal by their public key from the\n> allowedSignersFile. If the key is found then we do a verify. Otherwise\n> we only validate the signature but can not verify the signers identity.\n\nIs this the same behavior as GPG signing in Git?\n\n> Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\n> SIGNERS\") which contains valid public keys and a principal (usually\n> user@domain). Depending on the environment this file can be managed by\n> the individual developer or for example generated by the central\n> repository server from known ssh keys with push access. If the\n> repository only allows signed commits / pushes then the file can even be\n> stored inside it.\n\nStoring the allowedSignersFile in the repo is technically possible even\nif the repository does not allow signed commits/pushes, right? I would\nreword the last sentence as \"This file is usually stored outside the\nrepository, but if the repository only allows signed commits/pushes, the\nuser might choose to store it in the repository\".\n\n> Using SSH CA Keys with these files is also possible. Add\n> \"cert-authority\" as key option between the principal and the key to mark\n> it as a CA and all keys signed by it as valid for this CA.\n\nIs this functionality provided by SSH? I don't see \"cert-authority\"\nanywhere in the diff below.\n\nAlso, I notice that the tests are all provided at the end. I think that\nit would be better for the tests to be incrementally provided along with\nthe commit that introduces the relevant functionality, so it is clearer\nto the reviewers how it is supposed to work (and also for us to observe\ntest coverage).\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index a34742513ac..62b11c5f3a4 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n>  {\n>  \tint status = parse_hide_refs_config(var, value, \"receive\");\n>  \n> +\tgit_gpg_config(var, value, NULL);\n> +\n>  \tif (status)\n>  \t\treturn status;\n\nCheck the return value of git_gpg_config() to see if that config was\nprocessed by that function - if yes, we can return early.\n\n> +static void parse_ssh_output(struct signature_check *sigc)\n> +{\n> +\tconst char *line, *principal, *search;\n> +\n> +\t/*\n> +\t * ssh-keysign output should be:\n> +\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n> +\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n> +\t * or for valid but unknown keys:\n> +\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n> +\t */\n\nIs this \"ssh-keysign\" or \"ssh-keygen\" output?\n\nAlso, is this output documented to be stable even across locales?\n\n> +\tsigc->result = 'B';\n> +\tsigc->trust_level = TRUST_NEVER;\n\nA discussion of trust levels should also be in the commit message or\nuser documentation.\n\n> +\tif (!strcmp(var, \"gpg.ssh.revocationFile\")) {\n\nThe \"F\" in \"revocationFile\" has to be lowercase. (If tests were\nincluded, as I suggested above, it might have been easier to catch\nthis.)\n"},{"id":"431449","messageId":"xmqqlf5q6ils.fsf@gitster.g","threadId":"56054","inReplyTo":"20210728223206.2715554-1-jonathantanmy@google.com","subject":"Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-29T00:58:55Z","receivedAt":"2021-07-29T00:58:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonathan Tan <jonathantanmy@google.com> writes:\n\n>> -\tfmt = get_format_by_sig(signature);\n>> -\tif (!fmt)\n>> -\t\tBUG(\"bad signature '%s'\", signature);\n>\n> Here is the difference in functionality that I spotted. Here, lack of\n> fmt is fatal...\n>\n>> +\tfmt = get_format_by_sig(signature);\n>> +\tif (!fmt)\n>> +\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n>\n> ...but here it is not.\n\nWhile I was reviewing this step, I was assumign that the callers\nwould respond to this error return appropriately.  If it is not the\ncase, then we do have to fix that.\n\nThe original's use of BUG() is wrong in any case, I woud think.  The\n\"signature\" there is an external input, so we were reporting a data\nerror (it should have been die()), not a program logic error.\n\nThanks.\n"},{"id":"431450","messageId":"xmqqh7ge6ih7.fsf@gitster.g","threadId":"56054","inReplyTo":"20210728224523.2716969-1-jonathantanmy@google.com","subject":"Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-29T01:01:40Z","receivedAt":"2021-07-29T01:01:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonathan Tan <jonathantanmy@google.com> writes:\n\n>> +/*\n>> + * Strip CR from the line endings, in case we are on Windows.\n>> + * NEEDSWORK: make it trim only CRs before LFs and rename\n>> + */\n>> +static void remove_cr_after(struct strbuf *buffer, size_t offset)\n>> +{\n>> +\tsize_t i, j;\n>> +\n>> +\tfor (i = j = offset; i < buffer->len; i++) {\n>> +\t\tif (buffer->buf[i] != '\\r') {\n>> +\t\t\tif (i != j)\n>> +\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n>> +\t\t\tj++;\n>> +\t\t}\n>> +\t}\n>> +\tstrbuf_setlen(buffer, j);\n>> +}\n>\n> In the future, I would prefer refactoring like this to be in its own\n> patch. For the moment, this should probably be called \"remove_cr\" (no\n> \"after\" as CRs are removed wherever they are in the string).\n\nYou have me to blame for that \"after\".  It was meant to signal that\nCR's before the given \"offset\" are retained.\n\n> A config that has 2 modes of operation is quite error-prone, I think.\n> For example, a user could put a path starting with \"ssh-\" (admittedly\n> unlikely since it would usually be an absolute path, but not\n> impossible). And also from an implementation point of view, here the\n> \"ssh-\" is case-sensitive, but in a future patch, there is a \"ssh-\" that\n> is case-insensitive.\n>\n> Can this just always take a path?\n\nSensible simplification, I guess.\n\nThanks for a careful review.\n\n>> +\tif (ret) {\n>> +\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n>> +\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n>> +\n>> +\t\terror(\"%s\", signer_stderr.buf);\n>> +\t\tgoto out;\n>> +\t}\n>\n> Checking for \"usage:\" seems fragile -  a binary running in a different\n> locale might emit a different string, and legitimate output may somehow\n> contain the string \"usage:\". Is there a different way to detect a\n> version mismatch?\n\n"},{"id":"431466","messageId":"6a831f56-3072-be97-8a24-023f034f597b@gigacodes.de","threadId":"56054","inReplyTo":"xmqqlf5q6ils.fsf@gitster.g","subject":"Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T07:44:47Z","receivedAt":"2021-07-29T07:44:56Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 02:58, Junio C Hamano wrote:\n> Jonathan Tan <jonathantanmy@google.com> writes:\n> \n>>> -\tfmt = get_format_by_sig(signature);\n>>> -\tif (!fmt)\n>>> -\t\tBUG(\"bad signature '%s'\", signature);\n>>\n>> Here is the difference in functionality that I spotted. Here, lack of\n>> fmt is fatal...\n>>\n>>> +\tfmt = get_format_by_sig(signature);\n>>> +\tif (!fmt)\n>>> +\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n>>\n>> ...but here it is not.\n> \n> While I was reviewing this step, I was assumign that the callers\n> would respond to this error return appropriately.  If it is not the\n> case, then we do have to fix that.\n> \n> The original's use of BUG() is wrong in any case, I woud think.  The\n> \"signature\" there is an external input, so we were reporting a data\n> error (it should have been die()), not a program logic error.\n> \n> Thanks.\n> \n\nMy intention was to actually change this behavior (i should have made \nthat clear in the commit message). When the current git version \nencounters an unknown signature format it will BUG() and leave the user \nwith a coredump.\nI assume that some repos will have multiple different signature formats \nin their history in the future and the effect i would have liked was to \nonly mark the commits with unknown signatures as bad/unknown when using \ngit log --show-signature for example.\nI have checked all the calls to check_signature and unfortunately the \nresult check is really inconsistent. Some ignore it completely, others \ncheck but still then dereference fields from the sigcheck struct.\nSo for now a die() is probably the correct way to go.\n\nIn the future we might want to differentiate between verifying a single \ncommit (in which case we can die()) and a list of commits. Or fix all \nthe calls to check_signature to check the return code.\n\nThanks\n"},{"id":"431467","messageId":"6b244afb-e4bb-c613-142a-4baba1149de3@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-07-29T08:19:22Z","receivedAt":"2021-07-29T08:19:37Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 29/07/21 02.36, Fabian Stelzer via GitGitGadget wrote:\n> openssh 8.7 will add valid-after, valid-before options to the allowed keys\n> keyring. This allows us to pass the commit timestamp to the verification\n> call and make key rollover possible and still be able to verify older\n> commits. Set valid-after=NOW when adding your key to the keyring and set\n> valid-before to make it fail if used after a certain date. Software like\n> gitolite/github or corporate automation can do this automatically when ssh\n> push keys are addded / removed I will add this feature in a follow up patch\n> afterwards.\n> \n\nI read above as \"set valid-before=<some date> and valid-after=<now> to \nlimit key validity for several days from now\". Is it right?\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"431468","messageId":"e79a63f3-d86b-d4e7-2aa5-12aa1f2e6090@gigacodes.de","threadId":"56054","inReplyTo":"xmqqeebi86m7.fsf@gitster.g","subject":"Re: [PATCH v6 4/9] ssh signing: provide a textual representation of the signing key","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T08:21:04Z","receivedAt":"2021-07-29T08:21:09Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 28.07.21 23:34, Junio C Hamano wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\n>> in push certs and textual output we prefer the ssh fingerprint instead.\n> \n> These sentences that lack the initial capital letters would look\n> unusual and distracting in our \"git log --no-merges\" stream.\n> \n\nFixed\n\n>>   \n>> +/* Returns a textual but unique representation ot the signing key */\n> \n> \"ot\" -> \"of\".\n> \n\nFixed\n\n>> +const char *get_signing_key_id(void)\n>> +{\n>> +\tif (!strcmp(use_format->name, \"ssh\")) {\n>> +\t\treturn get_ssh_key_fingerprint(get_signing_key());\n>> +\t} else {\n>> +\t\t/* GPG/GPGSM only store a key id on this variable */\n>> +\t\treturn get_signing_key();\n> \n> Hmph, we could ask gpg key fingerprint if we wanted to, and we\n> cannot tell why \"ssh\" side needs a separate \"key\" and \"key_id\"\n> while \"gpg\" side does not.  Hopefully it will become clear as we\n> read on?\n> \n> Again, dispatching on use_format->name looked rather unexpected.\n> \n\ni will put the two strcmp(ssh) ifs on my todo list to also replace with \na callback function.\n\n>> -\tchar *signing_key = xstrdup(get_signing_key());\n>> +\tchar *signing_key_id = xstrdup(get_signing_key_id());\n>>   \tconst char *cp, *np;\n>>   \tstruct strbuf cert = STRBUF_INIT;\n>>   \tint update_seen = 0;\n>>   \n>>   \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n>> -\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n>> +\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n> \n> Ahh...  We do not send GPG fingerprint in push certificate but you\n> want to use the fingerprint when signing with SSH keys, and that is\n> where the need for signing_key_id comes from?\n> \n> OK.\n> \n\nPreviously the push certs contained the configured user.signingkey as \n\"pusher\". For gpg this is usually the key id. (e.g.: ABCDEF01)\nFor ssh signing this can now be a file path which would not make much \nsense to put into the push cert. I did not use the public ssh key since \nthe file can also contain an encrypted private key, so i would have to \nask ssh-keygen for the public key anyway.\nSince the ssh fingerprint more resembles the gpg key id i used it instead.\n\nAs far as i understand the actual contents of the \"pusher\" header is not \nreally relevant for the push-cert. The unique nonce is important but \nbesides that its just a signed text blob.\nIf we don't care about having a local users file path in this header we \ncould drop this commit.\n"},{"id":"431469","messageId":"0da4ce02-9f2f-adc9-40ae-bc4120ef534c@gigacodes.de","threadId":"56054","inReplyTo":"20210728223206.2715554-1-jonathantanmy@google.com","subject":"Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T08:43:35Z","receivedAt":"2021-07-29T08:43:43Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 00:32, Jonathan Tan wrote:\n> I think this patch set is beyond the \"is this a good idea in general\"\n> phase (in particular, I think that being able to sign Git commits by\n> using SSH infrastructure is very useful), so I'll proceed to critiquing\n> the commits in more detail.\n\nThanks for your help.\n\n> \n> Firstly, in commit messages, the left side of the colon is usually the\n> name of the subsystem - in this case, \"gpg-interface\".\n> \n\nThe docs call this \"name of the component you're working on\". Since this \ncode does not actually change any gpg functionality (at least it should \nnot) i think gpg-interface in the commits might be a bit misleading.\n\n\n>> To be able to implement new signing formats this commit:\n>>   - makes the sigc structure more generic by renaming \"gpg_output\" to\n>>     \"output\"\n>>   - introduces function pointers in the gpg_format structure to call\n>>     format specific signing and verification functions\n>>   - moves format detection from verify_signed_buffer into the check_signature\n>>     api function and calls the format specific verify\n>>   - renames and wraps sign_buffer to handle format specific signing logic\n>>     as well\n> \n> I think that this commit should be further split up - in particular, it\n> is hard for reviewers to verify that there is no difference in\n> functionality before and after this commit. I already spotted one\n> difference - perhaps there are more. For me, splitting the above 4\n> points into 4 commits would be an acceptable split.\n> \n\nThe rename can of course be easily separated. The others would probably \nrequire some code in between commits that's not present in the final \npatch result to make the individual commits compile / work. Otherwise \nthose would only add unused code with the last commit then actually \nusing everything. I don't think that would make things easier to verify, \nwould it?\n\n\n> [snip]\n> \n>>   static struct gpg_format gpg_format[] = {\n>> -\t{ .name = \"openpgp\", .program = \"gpg\",\n>> -\t  .verify_args = openpgp_verify_args,\n>> -\t  .sigs = openpgp_sigs\n>> +\t{\n>> +\t\t.name = \"openpgp\",\n>> +\t\t.program = \"gpg\",\n>> +\t\t.verify_args = openpgp_verify_args,\n>> +\t\t.sigs = openpgp_sigs,\n>> +\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n>> +\t\t.sign_buffer = sign_buffer_gpg,\n>>   \t},\n>> -\t{ .name = \"x509\", .program = \"gpgsm\",\n>> -\t  .verify_args = x509_verify_args,\n>> -\t  .sigs = x509_sigs\n>> +\t{\n>> +\t\t.name = \"x509\",\n>> +\t\t.program = \"gpgsm\",\n>> +\t\t.verify_args = x509_verify_args,\n>> +\t\t.sigs = x509_sigs,\n>> +\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n>> +\t\t.sign_buffer = sign_buffer_gpg,\n>>   \t},\n>>   };\n> \n> I think that verify_signed_buffer and sign_buffer should replace\n> verify_args and sigs, not be alongside them. In particular, I see from\n> later patches that a new entry will be introduced for SSH, and the\n> corresponding new \"verify\" function does not use verify_args or sigs.\n> \n\nI kept the verify_args since i would either have to duplicate the \nverify_gpg_signed_buffer for gpg & gpgsm or have an if within deciding \nwhat format to use.\nAlso this is something that we might want to make a configuration option \nin the future and pass to ssh-keygen as well (there are a couple of -O \noptions for it users might want)\n\nsigs is still needed for the parse_signed_buffer api function.\n"},{"id":"431470","messageId":"f3e72ec9-3ed4-9955-a7bd-042fa6eb016c@gigacodes.de","threadId":"56054","inReplyTo":"20210728224832.2717826-1-jonathantanmy@google.com","subject":"Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T08:59:46Z","receivedAt":"2021-07-29T08:59:53Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 00:48, Jonathan Tan wrote:\n>> if user.signingkey is not set and a ssh signature is requested we call\n>> ssh-add -L and use the first key we get\n> \n> [snip]\n> \n> Could the commit message have a better explanation of why we need this?\n> (Also, I would think that the command being run needs to be configurable\n> instead of being just the first \"ssh-add\" in $PATH, and the parsing of\n> the output should be more rigorous. But this is moot if we don't need\n> this feature in the first place.)\n> \n\nHow about:\nIf user.signingkey ist not set and a ssh signature is requested we call \nssh-add -L und use the first key we get. This enables us to activate \ncommit signing globally for all users on a shared server when ssh-agent \nforwarding is already in use without the need to touch an individual \nusers gitconfig.\n\nMaybe a general gpg.ssh.signingKeyDefaultCommand that we call and use \nthe first returned line as key would be useful and achieve the same goal \nwithout having this default for everyone.\nOn the other hand i like having less configuration / good defaults for \nindividual users. But I'm coming from a corporate environment, not an \nopen source project.\n"},{"id":"431471","messageId":"54671c83-4b1f-5e24-a6ad-226a4f45f952@gigacodes.de","threadId":"56054","inReplyTo":"xmqq35ry85or.fsf@gitster.g","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T09:12:27Z","receivedAt":"2021-07-29T09:12:33Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 28.07.21 23:55, Junio C Hamano wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> to verify a ssh signature we first call ssh-keygen -Y find-principal to\n> \n> \"to\" -> \"To\".\n\nfixed\n\n[snip]\n\n>> +\t/*\n>> +\t * ssh-keysign output should be:\n>> +\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n>> +\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n> \n> A bit unfortunate line that is overly long.  These two are not\n> mutually exclusive two different choices, but one is a special case\n> of the other, no?  How about phrasing it like so instead?\n> \n> \t/*\n> \t * ssh-keysign output should be:\n> \t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n>           *\n> \t * or for valid but unknown keys:\n> \t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n>           *\n> \t * Note that \"PRINCIPAL\" can contain whitespace, \"RSA\" and\n> \t * \"SHA256\" part could be a different token that names of\n> \t * the algorithms used, and \"FINGERPRINT\" is a hexadecimal\n>           * string.  By finding the last occurence of \" with \", we can\n>           * reliably parse out the PRINCIPAL.\n> \t */\n> \n\nYes, it's a special case that makes it a bit harder to parse. I will \nchange the comment like you suggested. That makes it clear.\n\n>> +\t\t/* Search for the last \"with\" to get the full principal */\n>> +\t\tprincipal = line;\n>> +\t\tdo {\n>> +\t\t\tsearch = strstr(line, \" with \");\n>> +\t\t\tif (search)\n>> +\t\t\t\tline = search + 1;\n>> +\t\t} while (search != NULL);\n>> +\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n>> +\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n> \n> OK.  This does not care the \"RSA\" part, which is future resistant.\n> It assumes the <algo>:<fingerprint> comes after literal \" key \",\n> which I think is a reasonable thing to do.\n> \n> However, we never checked if the line has \"key\" in it, so\n> strstr(line, \"key\") + 4 may not be pointing at where this code\n> expects.\n> \n\nHmm. What would i do if i don't find \"key\"? Still mark the signature as \nvalid an just leave fingerprint & key empty?\n\n>> +\t\t\t/* We found principals - Try with each until we find a\n>> +\t\t\t * match */\n> \n>                          /*\n>                           * Do not forget our multi-line comment\n>                           * style, please.\n>                           */\n> \n\nfixed. clang-format wordwrapped those :/\n\n\n>> +\n>> +\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n> \n> This is somewhat unusual construct in our codebase, I suspect.  And\n> probably is even wrong.  Didn't you mean\n> \n> \t\t\tif (!ret)\n> \t\t\t\tret = starts_with(...);\n> \n> instead?  Surely, when pipe_command() failed, it is likely that\n> ssh_keygen_out may not have anything useful, and checking what the\n> first up-to-four bytes of it contain unconditionally may be cheap\n> enough, but the person reading the code would expect you to peek\n> into the result only when you actually got the result, no?\n\nyou are correct. we don't need to look at the output when the command fails.\n\n> \n>> +\t\t\tif (ret == 0)\n>> +\t\t\t\tbreak;\n> \n> It's more common to do\n> \n> \t\t\tif (!ret)\n> \t\t\t\tbreak;\n> \n\nchanged.\n\nThanks\n"},{"id":"431472","messageId":"d4bda019-bbea-6645-e46a-18a702d3f0ad@gigacodes.de","threadId":"56054","inReplyTo":"20210728230452.2719333-1-jonathantanmy@google.com","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T09:48:18Z","receivedAt":"2021-07-29T09:48:25Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 01:04, Jonathan Tan wrote:\n>> to verify a ssh signature we first call ssh-keygen -Y find-principal to\n>> look up the signing principal by their public key from the\n>> allowedSignersFile. If the key is found then we do a verify. Otherwise\n>> we only validate the signature but can not verify the signers identity.\n> \n> Is this the same behavior as GPG signing in Git?\n\nNot quite. GPG requires every signers public key to be in the keyring. \nBut even then, the \"UNDEFINED\" Trust level is enough to be valid for \ncommits (but not for merges).\nFor SSH i did set the unknown keys to UNDEFINED as well and they will \nshow up as valid but not have a principal to identify them.\nThis way a project can decide wether to accept unknown keys by setting \nthe gpg.mintrustlevel. So the default behaviour is different.\nThe alternative would be to treat unknown keys always as invalid.\n\n> \n>> Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\n>> SIGNERS\") which contains valid public keys and a principal (usually\n>> user@domain). Depending on the environment this file can be managed by\n>> the individual developer or for example generated by the central\n>> repository server from known ssh keys with push access. If the\n>> repository only allows signed commits / pushes then the file can even be\n>> stored inside it.\n> \n> Storing the allowedSignersFile in the repo is technically possible even\n> if the repository does not allow signed commits/pushes, right? I would\n> reword the last sentence as \"This file is usually stored outside the\n> repository, but if the repository only allows signed commits/pushes, the\n> user might choose to store it in the repository\".\n\nyes, thats correct. I have changed the wording.\n\n> \n>> Using SSH CA Keys with these files is also possible. Add\n>> \"cert-authority\" as key option between the principal and the key to mark\n>> it as a CA and all keys signed by it as valid for this CA.\n> \n> Is this functionality provided by SSH? I don't see \"cert-authority\"\n> anywhere in the diff below.\n\nI'll add \"See \"CERTIFICATES\" in ssh-keygen(1).\"\nIt is a SSH feature that i just wanted to make people aware of.\n\n> \n> Also, I notice that the tests are all provided at the end. I think that\n> it would be better for the tests to be incrementally provided along with\n> the commit that introduces the relevant functionality, so it is clearer\n> to the reviewers how it is supposed to work (and also for us to observe\n> test coverage).\n\nThe problem is that nearly all of the tests use both signing & \nverification of signatures. I could move the initial test that creates \nall the signed commits but probably not much else.\n\n>> +\tgit_gpg_config(var, value, NULL);\n> \n> Check the return value of git_gpg_config() to see if that config was\n> processed by that function - if yes, we can return early.\n> \n\nfixed\n\n\n>> +static void parse_ssh_output(struct signature_check *sigc)\n>> +{\n>> +\tconst char *line, *principal, *search;\n>> +\n>> +\t/*\n>> +\t * ssh-keysign output should be:\n>> +\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n>> +\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n>> +\t * or for valid but unknown keys:\n>> +\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n>> +\t */\n> \n> Is this \"ssh-keysign\" or \"ssh-keygen\" output?\n\nssh-keygen. ssh-keysign is only used for host keys. But the names can \nget a bit confusing sometimes. i changed it to ssh-keygen here.\n\n> \n> Also, is this output documented to be stable even across locales?\n\nNot really :/ (it currently is not locale specific)\nThe documentation states to only check the commands exit code. Do we \ntrust the exit code enough to rely on it for verification?\nIf so then i can move the main result and only parse the text for the \nsigner/fingerprint info thats used in log formats. This way only the \nlogs would break in case the output changes.\n\nI added the output check since the gpg code did so as well:\nret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n\n> \n>> +\tsigc->result = 'B';\n>> +\tsigc->trust_level = TRUST_NEVER;\n> \n> A discussion of trust levels should also be in the commit message or\n> user documentation.\n> \n>> +\tif (!strcmp(var, \"gpg.ssh.revocationFile\")) {\n> \n> The \"F\" in \"revocationFile\" has to be lowercase. (If tests were\n> included, as I suggested above, it might have been easier to catch\n> this.)\n> \n\nfixed\n\nThanks\n"},{"id":"431475","messageId":"ea422afa-50c0-4345-e7e6-935d02608c39@gigacodes.de","threadId":"56054","inReplyTo":"20210728224523.2716969-1-jonathantanmy@google.com","subject":"Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T11:01:20Z","receivedAt":"2021-07-29T11:01:27Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 00:45, Jonathan Tan wrote:\n> Keep the commit titles to 50 characters or fewer. E.g.:\n> \n>    gpg-interface: teach \"ssh\" gpg.format\n> \n\ni will go over my commits and shorten them although I find your example \nvery unclear. or did you mean: teach \"ssh\" to gpg.format ?\n\n>> implements the actual sign_buffer_ssh operation and move some shared\n>> cleanup code into a strbuf function\n> \n> Capitalization and punctuation.\nfixed\n\n> \n>> Set gpg.format = ssh and user.signingkey to either a ssh public key\n>> string (like from an authorized_keys file), or a ssh key file.\n>> If the key file or the config value itself contains only a public key\n>> then the private key needs to be available via ssh-agent.\n>>\n>> gpg.ssh.program can be set to an alternative location of ssh-keygen.\n>> A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\n>> this feature. Since only ssh-keygen is needed it can this way be\n>> installed seperately without upgrading your system openssh packages.\n> \n> I notice that end-user documentation (e.g. about gpg.ssh.program) is in\n> its own patch, but could that be added as functionality is being\n> implemented? That makes it easier for reviewers to understand what's\n> being implemented in each patch.\n> \n\nI can move the user.signingkey & gpg.format part into the signing \nimplementation commit and the rest into the verification. I don't see \nmuch benefit in splitting it up further. I don't want to split up parts \nof the same documentation block into separate commits.\n\n>> +\n>> +\tif (starts_with(signing_key, \"ssh-\")) {\n>> +\t\t/* A literal ssh key */\n>> +\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n>> +\t\tif (!key_file)\n>> +\t\t\treturn error_errno(\n>> +\t\t\t\t_(\"could not create temporary file\"));\n>> +\t\tkeylen = strlen(signing_key);\n>> +\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n>> +\t\t    close_tempfile_gently(key_file) < 0) {\n>> +\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n>> +\t\t\t\t    key_file->filename.buf);\n>> +\t\t\tgoto out;\n>> +\t\t}\n>> +\t\tssh_signing_key_file = key_file->filename.buf;\n>> +\t} else {\n>> +\t\t/* We assume a file */\n>> +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n>> +\t}\n> \n> A config that has 2 modes of operation is quite error-prone, I think.\n> For example, a user could put a path starting with \"ssh-\" (admittedly\n> unlikely since it would usually be an absolute path, but not\n> impossible). And also from an implementation point of view, here the\n> \"ssh-\" is case-sensitive, but in a future patch, there is a \"ssh-\" that\n> is case-insensitive.\n> \n> Can this just always take a path?\n> \n\nI found the ability to specify the key literally useful since i don't \nneed an extra file for my public key. In my case all keys come from an \nssh-agent anyway but I'd like to be able to select which one to use for \nsigning. But i'm not hard pressed on this feature. If consenus is this \ncomplicates things then i can remove it.\n\n>> +\tif (ret) {\n>> +\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n>> +\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n>> +\n>> +\t\terror(\"%s\", signer_stderr.buf);\n>> +\t\tgoto out;\n>> +\t}\n> \n> Checking for \"usage:\" seems fragile -  a binary running in a different\n> locale might emit a different string, and legitimate output may somehow\n> contain the string \"usage:\". Is there a different way to detect a\n> version mismatch?\n> \n\nI agree. Unfortunately i did not find any better way. But i think the \nrisk of doing something wrong here is quite low. We only check for \n\"usage:\" in case ssh-keygen fails. And all we do if we find it is give \nthe user an extra hint on what the problem probably is.\nIn any case we print the full stderr output as well.\n"},{"id":"431477","messageId":"7689f8c6-0ac7-0121-4034-c8747edaad05@gigacodes.de","threadId":"56054","inReplyTo":"6b244afb-e4bb-c613-142a-4baba1149de3@gmail.com","subject":"Re: [PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T11:03:18Z","receivedAt":"2021-07-29T11:03:29Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 10:19, Bagas Sanjaya wrote:\n> On 29/07/21 02.36, Fabian Stelzer via GitGitGadget wrote:\n>> openssh 8.7 will add valid-after, valid-before options to the allowed \n>> keys\n>> keyring. This allows us to pass the commit timestamp to the verification\n>> call and make key rollover possible and still be able to verify older\n>> commits. Set valid-after=NOW when adding your key to the keyring and set\n>> valid-before to make it fail if used after a certain date. Software like\n>> gitolite/github or corporate automation can do this automatically when \n>> ssh\n>> push keys are addded / removed I will add this feature in a follow up \n>> patch\n>> afterwards.\n>>\n> \n> I read above as \"set valid-before=<some date> and valid-after=<now> to \n> limit key validity for several days from now\". Is it right?\n> \n\nno. \"NOW\" is not meant literally but in the sense to add the current \ndate when adding the key. I'll edit the description. But this feature in \ngeneral will follow in a separate patchset with proper documentation anyway.\n"},{"id":"431482","messageId":"76cc0f7d-90d9-18bf-e749-feff8e584453@gigacodes.de","threadId":"56054","inReplyTo":"d4bda019-bbea-6645-e46a-18a702d3f0ad@gigacodes.de","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T13:52:40Z","receivedAt":"2021-07-29T13:52:51Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 11:48, Fabian Stelzer wrote:\n> On 29.07.21 01:04, Jonathan Tan wrote:\n>>> to verify a ssh signature we first call ssh-keygen -Y find-principal to\n>>> look up the signing principal by their public key from the\n>>> allowedSignersFile. If the key is found then we do a verify. Otherwise\n>>> we only validate the signature but can not verify the signers identity.\n>>\n>> Is this the same behavior as GPG signing in Git?\n> \n> Not quite. GPG requires every signers public key to be in the keyring. \n> But even then, the \"UNDEFINED\" Trust level is enough to be valid for \n> commits (but not for merges).\n> For SSH i did set the unknown keys to UNDEFINED as well and they will \n> show up as valid but not have a principal to identify them.\n> This way a project can decide wether to accept unknown keys by setting \n> the gpg.mintrustlevel. So the default behaviour is different.\n> The alternative would be to treat unknown keys always as invalid.\n> \n\nI thought a bit more about this and my approach is indeed problematic \nespecially when a repo has both gpg and ssh signatures. The trust level \nsetting can then not behave differently for both.\n\nMy intention of still showing valid but unknown signatures in the log as \nok (but unknown) was to encourage users to always sign their work even \nif they are not (yet) trusted in the allowedSignersFile.\n\nI think the way forward should be to treat unknown singing keys as not \nverified like gpg does.\n\nIf a ssh key is verified and in the allowedSignersFile i would still set \nits trust level to \"FULLY\".\n"},{"id":"431505","messageId":"YQL8zAHe8CkW1U6j@google.com","threadId":"56054","inReplyTo":"f05bab16096c080891ee8f7e179eecce7f32e839.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2021-07-29T19:09:00Z","receivedAt":"2021-07-29T19:09:10Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Thanks for this series, it sounds like a great idea. I have a few\ncomments, inline below.\n\nOn 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:\n[snip]\n> +static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n> +\t\t\t   const char *signing_key)\n> +{\n> +\tstruct child_process signer = CHILD_PROCESS_INIT;\n> +\tint ret = -1;\n> +\tsize_t bottom, keylen;\n> +\tstruct strbuf signer_stderr = STRBUF_INIT;\n> +\tstruct tempfile *key_file = NULL, *buffer_file = NULL;\n> +\tchar *ssh_signing_key_file = NULL;\n> +\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n> +\n> +\tif (!signing_key || signing_key[0] == '\\0')\n> +\t\treturn error(\n> +\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n> +\n> +\tif (starts_with(signing_key, \"ssh-\")) {\n> +\t\t/* A literal ssh key */\n> +\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n> +\t\tif (!key_file)\n> +\t\t\treturn error_errno(\n> +\t\t\t\t_(\"could not create temporary file\"));\n> +\t\tkeylen = strlen(signing_key);\n> +\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n> +\t\t    close_tempfile_gently(key_file) < 0) {\n> +\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n> +\t\t\t\t    key_file->filename.buf);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tssh_signing_key_file = key_file->filename.buf;\n\nYou probably want to call strbuf_detach() here, because...\n\n> +\t} else {\n> +\t\t/* We assume a file */\n> +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n> +\t}\n\n... you need to free the memory returned by expand_user_path(). If you\ndetach the strbuf above, you can unconditionally\nfree(ssh_signing_key_file) at the end of this function.\n\n> +\n> +\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n> +\tif (!buffer_file) {\n> +\t\terror_errno(_(\"could not create temporary file\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n> +\t    close_tempfile_gently(buffer_file) < 0) {\n> +\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n> +\t\t\t    buffer_file->filename.buf);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tstrvec_pushl(&signer.args, use_format->program,\n> +\t\t     \"-Y\", \"sign\",\n> +\t\t     \"-n\", \"git\",\n> +\t\t     \"-f\", ssh_signing_key_file,\n> +\t\t     buffer_file->filename.buf,\n> +\t\t     NULL);\n> +\n> +\tsigchain_push(SIGPIPE, SIG_IGN);\n> +\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n> +\tsigchain_pop(SIGPIPE);\n> +\n> +\tif (ret) {\n> +\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n> +\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n\nI share Jonathan Tan's concern about checking for \"usage:\" in the stderr\noutput here. I think in patch 6 the tests rely on a specific return code\nto check that \"-Y sign\" is working as expected; can that be used here\ninstead?\n\n> +\n> +\t\terror(\"%s\", signer_stderr.buf);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tbottom = signature->len;\n> +\n> +\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n> +\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n> +\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {\n> +\t\terror_errno(\n> +\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n> +\t\t\tssh_signature_filename.buf);\n> +\t}\n> +\tunlink_or_warn(ssh_signature_filename.buf);\n> +\n> +\t/* Strip CR from the line endings, in case we are on Windows. */\n> +\tremove_cr_after(signature, bottom);\n> +\n> +out:\n> +\tif (key_file)\n> +\t\tdelete_tempfile(&key_file);\n> +\tif (buffer_file)\n> +\t\tdelete_tempfile(&buffer_file);\n> +\tstrbuf_release(&signer_stderr);\n> +\tstrbuf_release(&ssh_signature_filename);\n> +\treturn ret;\n> +}\n> -- \n> gitgitgadget\n> \n"},{"id":"431506","messageId":"YQL84R7qNv8pnHro@google.com","threadId":"56054","inReplyTo":"f3e72ec9-3ed4-9955-a7bd-042fa6eb016c@gigacodes.de","subject":"Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2021-07-29T19:09:21Z","receivedAt":"2021-07-29T19:09:31Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2021.07.29 10:59, Fabian Stelzer wrote:\n> On 29.07.21 00:48, Jonathan Tan wrote:\n> > > if user.signingkey is not set and a ssh signature is requested we call\n> > > ssh-add -L and use the first key we get\n> > \n> > [snip]\n> > \n> > Could the commit message have a better explanation of why we need this?\n> > (Also, I would think that the command being run needs to be configurable\n> > instead of being just the first \"ssh-add\" in $PATH, and the parsing of\n> > the output should be more rigorous. But this is moot if we don't need\n> > this feature in the first place.)\n> > \n> \n> How about:\n> If user.signingkey ist not set and a ssh signature is requested we call\n> ssh-add -L und use the first key we get. This enables us to activate commit\n> signing globally for all users on a shared server when ssh-agent forwarding\n> is already in use without the need to touch an individual users gitconfig.\n> \n> Maybe a general gpg.ssh.signingKeyDefaultCommand that we call and use the\n> first returned line as key would be useful and achieve the same goal without\n> having this default for everyone.\n> On the other hand i like having less configuration / good defaults for\n> individual users. But I'm coming from a corporate environment, not an open\n> source project.\n\nDoesn't this run the risk of using the wrong key (and potentially\nexposing someone's identity)? On my work machine, my corporate SSH key\nis not actually the first key in my SSH agent.\n\nRather than making this behavior the default, could it instead be\nenabled only if the signing key is set to \"use-ssh-agent\" or something\nsimilar?\n"},{"id":"431507","messageId":"YQL8+UFtVJPlJroe@google.com","threadId":"56054","inReplyTo":"18a26ca49e7a9b0046559ac8d5c62c99ea7262ae.1627501009.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 6/9] ssh signing: add test prereqs","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2021-07-29T19:09:45Z","receivedAt":"2021-07-29T19:09:56Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:\n> From: Fabian Stelzer <fs@gigacodes.de>\n> \n> generate some ssh keys and a allowedSignersFile for testing\n> \n> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n> ---\n>  t/lib-gpg.sh | 29 +++++++++++++++++++++++++++++\n>  1 file changed, 29 insertions(+)\n> \n> diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\n> index 9fc5241228e..600c8d1a026 100644\n> --- a/t/lib-gpg.sh\n> +++ b/t/lib-gpg.sh\n> @@ -87,6 +87,35 @@ test_lazy_prereq RFC1991 '\n>  \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n>  '\n>  \n> +test_lazy_prereq GPGSSH '\n> +\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n> +\ttest $? != 127 || exit 1\n> +\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n> +\ttest $? = 0 || exit 1;\n> +\tmkdir -p \"${GNUPGHOME}\" &&\n> +\tchmod 0700 \"${GNUPGHOME}\" &&\n> +\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n> +\techo \"\\\"principal with number 1\\\" $(cat \"${GNUPGHOME}/ed25519_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n> +\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n> +\techo \"\\\"principal with number 2\\\" $(cat \"${GNUPGHOME}/rsa_2048_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n> +\tssh-keygen -t ed25519 -N \"super_secret\" -C \"git ed25519 encrypted key\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n> +\techo \"\\\"principal with number 3\\\" $(cat \"${GNUPGHOME}/protected_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n> +\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n> +\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n> +'\n> +\n> +SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n> +SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n> +SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n> +SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n> +SIGNING_KEY_PASSPHRASE=\"super_secret\"\n> +SIGNING_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n> +\n> +GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n> +GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n> +KEY_NOT_TRUSTED=\"No principal matched\"\n> +BAD_SIGNATURE=\"Signature verification failed\"\n> +\n\nIs there a reason why we don't use these variables in the script above?\n\nAlso, in general I feel that it's better to add tests in the same commit\nwhere new features are added, rather than having standalone test\ncommits.\n\n\n>  sanitize_pgp() {\n>  \tperl -ne '\n>  \t\t/^-----END PGP/ and $in_pgp = 0;\n> -- \n> gitgitgadget\n> \n"},{"id":"431516","messageId":"xmqqwnp851y6.fsf@gitster.g","threadId":"56054","inReplyTo":"YQL84R7qNv8pnHro@google.com","subject":"Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-29T19:56:17Z","receivedAt":"2021-07-29T19:56:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n> Rather than making this behavior the default, could it instead be\n> enabled only if the signing key is set to \"use-ssh-agent\" or something\n> similar?\n\nInteresting.  But is it too much trouble to find out the string that\nis used to identify the ssh key you want to use to sign, which would\nmake it worth supporting \"use-ssh-agent\" feature?  Unless you want\nto use multiple keys in a single project, and choose one of them\ndepending on whatever condition, and find it convenient to specify\nthe key-of-the-day by loading it to your ssh-agent, I do not quite\nsee why you'd want to explicitly configure it to \"use-ssh-agent\" and\nnot the actual key (either the textual key itself or some key-id to\nchoose one of your keys).  Care to clarify your expected use case a\nbit more?\n\nThanks.\n\n"},{"id":"431517","messageId":"xmqqsfzw51wq.fsf@gitster.g","threadId":"56054","inReplyTo":"YQL8+UFtVJPlJroe@google.com","subject":"Re: [PATCH v6 6/9] ssh signing: add test prereqs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-29T19:57:09Z","receivedAt":"2021-07-29T19:57:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n>> ...\n>> +GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n>> +KEY_NOT_TRUSTED=\"No principal matched\"\n>> +BAD_SIGNATURE=\"Signature verification failed\"\n>> +\n>\n> Is there a reason why we don't use these variables in the script above?\n>\n> Also, in general I feel that it's better to add tests in the same commit\n> where new features are added, rather than having standalone test\n> commits.\n\nAgain, good suggestions.\n\nThanks for excellent reviews.\n\n"},{"id":"431521","messageId":"xmqqczr04zr1.fsf@gitster.g","threadId":"56054","inReplyTo":"54671c83-4b1f-5e24-a6ad-226a4f45f952@gigacodes.de","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-29T20:43:46Z","receivedAt":"2021-07-29T20:43:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n>>> +\t\t/* Search for the last \"with\" to get the full principal */\n>>> +\t\tprincipal = line;\n>>> +\t\tdo {\n>>> +\t\t\tsearch = strstr(line, \" with \");\n>>> +\t\t\tif (search)\n>>> +\t\t\t\tline = search + 1;\n>>> +\t\t} while (search != NULL);\n>>> +\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n>>> +\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n>> OK.  This does not care the \"RSA\" part, which is future resistant.\n>> It assumes the <algo>:<fingerprint> comes after literal \" key \",\n>> which I think is a reasonable thing to do.\n>> However, we never checked if the line has \"key\" in it, so\n>> strstr(line, \"key\") + 4 may not be pointing at where this code\n>> expects.\n>\n> Hmm. What would i do if i don't find \"key\"? Still mark the signature\n> as valid an just leave fingerprint & key empty?\n\nWe didn't get a satisfactory response from the ssh-keygen we expect\nthat tells us that the external tool successfully decided that the\nsignature is good or bad.  I would feel safer if we said we did not\nsee a good signature in such a case.\n"},{"id":"431522","messageId":"xmqq8s1o4zn8.fsf@gitster.g","threadId":"56054","inReplyTo":"d4bda019-bbea-6645-e46a-18a702d3f0ad@gigacodes.de","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-07-29T20:46:03Z","receivedAt":"2021-07-29T20:46:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n> On 29.07.21 01:04, Jonathan Tan wrote:\n>\n>> Also, is this output documented to be stable even across locales?\n> Not really :/ (it currently is not locale specific)\n\nWe probably want to defeat l10n of the message by spawning it in the\nC locale regardless.\n\n> The documentation states to only check the commands exit code. Do we\n> trust the exit code enough to rely on it for verification?\n\nIs the exit code sufficient to learn who signed it?  Without knowing\nthat, we cannot see if the principal is in or not in our keychain,\nno?\n\n> If so then i can move the main result and only parse the text for the\n> signer/fingerprint info thats used in log formats. This way only the \n> logs would break in case the output changes.\n>\n> I added the output check since the gpg code did so as well:\n> ret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n\nDoes ssh-keygen have a mode similar to gpg's --status-fd feature\nwhere its output is geared more towards being stable and marchine\nparseable than being human friendly, by the way?\n\nThanks.\n"},{"id":"431526","messageId":"039a01d784bc$e92568a0$bb7039e0$@nexbridge.com","threadId":"56054","inReplyTo":"xmqq8s1o4zn8.fsf@gitster.g","subject":"RE: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2021-07-29T21:01:26Z","receivedAt":"2021-07-29T21:01:37Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On July 29, 2021 4:46 PM, Junio wrote:\n>Fabian Stelzer <fs@gigacodes.de> writes:\n>\n>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>\n>>> Also, is this output documented to be stable even across locales?\n>> Not really :/ (it currently is not locale specific)\n>\n>We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>\n>> The documentation states to only check the commands exit code. Do we\n>> trust the exit code enough to rely on it for verification?\n>\n>Is the exit code sufficient to learn who signed it?  Without knowing that, we cannot see if the principal is in or not in our\nkeychain, no?\n\nHave we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n\n>> If so then i can move the main result and only parse the text for the\n>> signer/fingerprint info thats used in log formats. This way only the\n>> logs would break in case the output changes.\n>>\n>> I added the output check since the gpg code did so as well:\n>> ret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n>\n>Does ssh-keygen have a mode similar to gpg's --status-fd feature where its output is geared more towards being stable and marchine\n>parseable than being human friendly, by the way?\n\nI do not think this can be done in a platform independent way. Not every platform that has ssh-keygen conforms to the OpenSSH UI or\noutput - a particular annoyance I get daily.\n\n"},{"id":"431528","messageId":"8b8fafad-0c49-0d17-b8f4-3e797a3fc9b6@gigacodes.de","threadId":"56054","inReplyTo":"039a01d784bc$e92568a0$bb7039e0$@nexbridge.com","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T21:12:32Z","receivedAt":"2021-07-29T21:12:37Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 23:01, Randall S. Becker wrote:\n> On July 29, 2021 4:46 PM, Junio wrote:\n>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>\n>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>\n>>>> Also, is this output documented to be stable even across locales?\n>>> Not really :/ (it currently is not locale specific)\n>>\n>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>\n>>> The documentation states to only check the commands exit code. Do we\n>>> trust the exit code enough to rely on it for verification?\n>>\n>> Is the exit code sufficient to learn who signed it?  Without knowing that, we cannot see if the principal is in or not in our\n> keychain, no?\n> \n> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>\n\nTo find the principal (who signed it) we don't have to parse the output. \nSince verification is first a call to look up the principals matching \nthe signatures public key from the allowedSignersFile and then trying \nverification with each one we already know which one matched (usually \nthere is only one. I think multiples is only possible with an SSH CA).\nOf course this even more relies on the exit code of ssh-keygen.\n\nNot sure which is more portable and reliable. Parsing the textual output \nor the exit code. At the moment my patch does both.\n\n>>> If so then i can move the main result and only parse the text for the\n>>> signer/fingerprint info thats used in log formats. This way only the\n>>> logs would break in case the output changes.\n>>>\n>>> I added the output check since the gpg code did so as well:\n>>> ret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n>>\n>> Does ssh-keygen have a mode similar to gpg's --status-fd feature where its output is geared more towards being stable and marchine\n>> parseable than being human friendly, by the way?\n> \n> I do not think this can be done in a platform independent way. Not every platform that has ssh-keygen conforms to the OpenSSH UI or\n> output - a particular annoyance I get daily.\n> \n"},{"id":"431530","messageId":"655f49be-7752-ca07-e9dd-9923300096ba@gigacodes.de","threadId":"56054","inReplyTo":"YQL84R7qNv8pnHro@google.com","subject":"Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T21:21:17Z","receivedAt":"2021-07-29T21:21:22Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 21:09, Josh Steadmon wrote:\n> On 2021.07.29 10:59, Fabian Stelzer wrote:\n>> On 29.07.21 00:48, Jonathan Tan wrote:\n>>>> if user.signingkey is not set and a ssh signature is requested we call\n>>>> ssh-add -L and use the first key we get\n>>>\n>>> [snip]\n>>>\n>>> Could the commit message have a better explanation of why we need this?\n>>> (Also, I would think that the command being run needs to be configurable\n>>> instead of being just the first \"ssh-add\" in $PATH, and the parsing of\n>>> the output should be more rigorous. But this is moot if we don't need\n>>> this feature in the first place.)\n>>>\n>>\n>> How about:\n>> If user.signingkey ist not set and a ssh signature is requested we call\n>> ssh-add -L und use the first key we get. This enables us to activate commit\n>> signing globally for all users on a shared server when ssh-agent forwarding\n>> is already in use without the need to touch an individual users gitconfig.\n>>\n>> Maybe a general gpg.ssh.signingKeyDefaultCommand that we call and use the\n>> first returned line as key would be useful and achieve the same goal without\n>> having this default for everyone.\n>> On the other hand i like having less configuration / good defaults for\n>> individual users. But I'm coming from a corporate environment, not an open\n>> source project.\n> \n> Doesn't this run the risk of using the wrong key (and potentially\n> exposing someone's identity)? On my work machine, my corporate SSH key\n> is not actually the first key in my SSH agent.\n> \n> Rather than making this behavior the default, could it instead be\n> enabled only if the signing key is set to \"use-ssh-agent\" or something\n> similar?\n> \n\nIf we introduce a signingKeyDefaultComand we don't need the \n\"use-ssh-agent\" flag.\n\nIf user.signingkey is set it is used no matter what. A private key needs \nto be available either in the specified file or via ssh agent.\n\nIf it is not set then an automatic way to get a default key would be great.\nSo if we set signingKeyDefaultCommand to \"ssh-add\" (or a script \nreturning a key) then the first available key could be used.\nIf this variable is unset and no user.signingkey is specified we fail \nand tell the user to set a signingkey.\n\nIf this variable is set to \"ssh-add\" by default or unset and needs to be\nset explicitly set to have an automatic default key can be decided.\n"},{"id":"431531","messageId":"309c7c9a-f38f-ca3b-ad30-4e22a27a692a@gigacodes.de","threadId":"56054","inReplyTo":"YQL8zAHe8CkW1U6j@google.com","subject":"Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T21:25:52Z","receivedAt":"2021-07-29T21:26:02Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 21:09, Josh Steadmon wrote:\n> Thanks for this series, it sounds like a great idea. I have a few\n> comments, inline below.\n>\n\nThanks for your review and help with this patch.\n\n> On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:\n> [snip]\n>> +\t\tssh_signing_key_file = key_file->filename.buf;\n> \n> You probably want to call strbuf_detach() here, because...\n> \n>> +\t} else {\n>> +\t\t/* We assume a file */\n>> +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n>> +\t}\n> \n> ... you need to free the memory returned by expand_user_path(). If you\n> detach the strbuf above, you can unconditionally\n> free(ssh_signing_key_file) at the end of this function.\n> \n\nfixed. thanks\n\n>> +\n>> +\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n>> +\tif (!buffer_file) {\n>> +\t\terror_errno(_(\"could not create temporary file\"));\n>> +\t\tgoto out;\n>> +\t}\n>> +\n>> +\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n>> +\t    close_tempfile_gently(buffer_file) < 0) {\n>> +\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n>> +\t\t\t    buffer_file->filename.buf);\n>> +\t\tgoto out;\n>> +\t}\n>> +\n>> +\tstrvec_pushl(&signer.args, use_format->program,\n>> +\t\t     \"-Y\", \"sign\",\n>> +\t\t     \"-n\", \"git\",\n>> +\t\t     \"-f\", ssh_signing_key_file,\n>> +\t\t     buffer_file->filename.buf,\n>> +\t\t     NULL);\n>> +\n>> +\tsigchain_push(SIGPIPE, SIG_IGN);\n>> +\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n>> +\tsigchain_pop(SIGPIPE);\n>> +\n>> +\tif (ret) {\n>> +\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n>> +\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n> \n> I share Jonathan Tan's concern about checking for \"usage:\" in the stderr\n> output here. I think in patch 6 the tests rely on a specific return code\n> to check that \"-Y sign\" is working as expected; can that be used here\n> instead?\n\nIn the test setup i first check if ssh-keygen at all is present (exit \ncode 127 means command not found). Afterwards i check for a specific \nerror message from the command if it is present. Not sure how portable \nthis is, but i can do that because i give known invalid parameters to \nit. I can't do this here without doing an additional call to ssh-keygen \njust to check this.\n\n> \n>> +\n>> +\t\terror(\"%s\", signer_stderr.buf);\n>> +\t\tgoto out;\n>> +\t}\n>> +\n>> +\tbottom = signature->len;\n>> +\n>> +\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n>> +\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n>> +\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {\n>> +\t\terror_errno(\n>> +\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n>> +\t\t\tssh_signature_filename.buf);\n>> +\t}\n>> +\tunlink_or_warn(ssh_signature_filename.buf);\n>> +\n>> +\t/* Strip CR from the line endings, in case we are on Windows. */\n>> +\tremove_cr_after(signature, bottom);\n>> +\n>> +out:\n>> +\tif (key_file)\n>> +\t\tdelete_tempfile(&key_file);\n>> +\tif (buffer_file)\n>> +\t\tdelete_tempfile(&buffer_file);\n>> +\tstrbuf_release(&signer_stderr);\n>> +\tstrbuf_release(&ssh_signature_filename);\n>> +\treturn ret;\n>> +}\n>> -- \n>> gitgitgadget\n>>\n"},{"id":"431532","messageId":"039b01d784c0$518b7440$f4a25cc0$@nexbridge.com","threadId":"56054","inReplyTo":"8b8fafad-0c49-0d17-b8f4-3e797a3fc9b6@gigacodes.de","subject":"RE: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2021-07-29T21:25:50Z","receivedAt":"2021-07-29T21:26:03Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On July 29, 2021 5:13 PM, Fabian Stelzer wrote:\n>Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures\n>\n>On 29.07.21 23:01, Randall S. Becker wrote:\n>> On July 29, 2021 4:46 PM, Junio wrote:\n>>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>>\n>>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>\n>>>>> Also, is this output documented to be stable even across locales?\n>>>> Not really :/ (it currently is not locale specific)\n>>>\n>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>>\n>>>> The documentation states to only check the commands exit code. Do we\n>>>> trust the exit code enough to rely on it for verification?\n>>>\n>>> Is the exit code sufficient to learn who signed it?  Without knowing\n>>> that, we cannot see if the principal is in or not in our\n>> keychain, no?\n>>\n>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>>\n>\n>To find the principal (who signed it) we don't have to parse the output.\n>Since verification is first a call to look up the principals matching the signatures public key from the allowedSignersFile and then trying\n>verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH\n>CA).\n>Of course this even more relies on the exit code of ssh-keygen.\n>\n>Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both.\n\nWhat about a configurable exit code for this? See the comment below about that.\n\n>>>> If so then i can move the main result and only parse the text for\n>>>> the signer/fingerprint info thats used in log formats. This way only\n>>>> the logs would break in case the output changes.\n>>>>\n>>>> I added the output check since the gpg code did so as well:\n>>>> ret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n>>>\n>>> Does ssh-keygen have a mode similar to gpg's --status-fd feature\n>>> where its output is geared more towards being stable and marchine parseable than being human friendly, by the way?\n>>\n>> I do not think this can be done in a platform independent way. Not\n>> every platform that has ssh-keygen conforms to the OpenSSH UI or output - a particular annoyance I get daily.\n>>\n\nWhat about a configurable command, like GIT_SSH_COMMAND to allow someone to plug in a mechanism or write something that supplies a result you can handle? That's something I could probably work out on my own platforms.\n\n"},{"id":"431533","messageId":"ef39f1f8-9da1-25e9-ec30-b7023705b58a@gigacodes.de","threadId":"56054","inReplyTo":"039b01d784c0$518b7440$f4a25cc0$@nexbridge.com","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-29T21:28:43Z","receivedAt":"2021-07-29T21:28:49Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 23:25, Randall S. Becker wrote:\n> On July 29, 2021 5:13 PM, Fabian Stelzer wrote:\n>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures\n>>\n>> On 29.07.21 23:01, Randall S. Becker wrote:\n>>> On July 29, 2021 4:46 PM, Junio wrote:\n>>>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>>>\n>>>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>>\n>>>>>> Also, is this output documented to be stable even across locales?\n>>>>> Not really :/ (it currently is not locale specific)\n>>>>\n>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>>>\n>>>>> The documentation states to only check the commands exit code. Do we\n>>>>> trust the exit code enough to rely on it for verification?\n>>>>\n>>>> Is the exit code sufficient to learn who signed it?  Without knowing\n>>>> that, we cannot see if the principal is in or not in our\n>>> keychain, no?\n>>>\n>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>>>\n>>\n>> To find the principal (who signed it) we don't have to parse the output.\n>> Since verification is first a call to look up the principals matching the signatures public key from the allowedSignersFile and then trying\n>> verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH\n>> CA).\n>> Of course this even more relies on the exit code of ssh-keygen.\n>>\n>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both.\n> \n> What about a configurable exit code for this? See the comment below about that.\n>\n\nI'm not sure what you mean. Something like \"treat exit(123) as success\"?\n\n>>>>> If so then i can move the main result and only parse the text for\n>>>>> the signer/fingerprint info thats used in log formats. This way only\n>>>>> the logs would break in case the output changes.\n>>>>>\n>>>>> I added the output check since the gpg code did so as well:\n>>>>> ret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n>>>>\n>>>> Does ssh-keygen have a mode similar to gpg's --status-fd feature\n>>>> where its output is geared more towards being stable and marchine parseable than being human friendly, by the way?\n>>>\n>>> I do not think this can be done in a platform independent way. Not\n>>> every platform that has ssh-keygen conforms to the OpenSSH UI or output - a particular annoyance I get daily.\n>>>\n> \n> What about a configurable command, like GIT_SSH_COMMAND to allow someone to plug in a mechanism or write something that supplies a result you can handle? That's something I could probably work out on my own platforms.\n> \n\nThis is already possible by setting gpg.ssh.program (although you'd have \nto pass the sign operation as well)\n"},{"id":"431534","messageId":"03a101d784c9$0cb413a0$261c3ae0$@nexbridge.com","threadId":"56054","inReplyTo":"ef39f1f8-9da1-25e9-ec30-b7023705b58a@gigacodes.de","subject":"RE: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2021-07-29T22:28:20Z","receivedAt":"2021-07-29T22:28:31Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On July 29, 2021 5:29 PM, Fabian Stelzer wrote:\n>On 29.07.21 23:25, Randall S. Becker wrote:\n>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote:\n>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and\n>>> verify signatures\n>>>\n>>> On 29.07.21 23:01, Randall S. Becker wrote:\n>>>> On July 29, 2021 4:46 PM, Junio wrote:\n>>>>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>>>>\n>>>>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>>>\n>>>>>>> Also, is this output documented to be stable even across locales?\n>>>>>> Not really :/ (it currently is not locale specific)\n>>>>>\n>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>>>>\n>>>>>> The documentation states to only check the commands exit code. Do\n>>>>>> we trust the exit code enough to rely on it for verification?\n>>>>>\n>>>>> Is the exit code sufficient to learn who signed it?  Without\n>>>>> knowing that, we cannot see if the principal is in or not in our\n>>>> keychain, no?\n>>>>\n>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>>>>\n>>>\n>>> To find the principal (who signed it) we don't have to parse the output.\n>>> Since verification is first a call to look up the principals matching\n>>> the signatures public key from the allowedSignersFile and then trying\n>>> verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH\n>CA).\n>>> Of course this even more relies on the exit code of ssh-keygen.\n>>>\n>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both.\n>>\n>> What about a configurable exit code for this? See the comment below about that.\n>>\n>\n>I'm not sure what you mean. Something like \"treat exit(123) as success\"?\n\nHow about gpg.ssh.successExit=123 or something like that.\n\n>>>>>> If so then i can move the main result and only parse the text for\n>>>>>> the signer/fingerprint info thats used in log formats. This way\n>>>>>> only the logs would break in case the output changes.\n>>>>>>\n>>>>>> I added the output check since the gpg code did so as well:\n>>>>>> ret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n>>>>>\n>>>>> Does ssh-keygen have a mode similar to gpg's --status-fd feature\n>>>>> where its output is geared more towards being stable and marchine parseable than being human friendly, by the way?\n>>>>\n>>>> I do not think this can be done in a platform independent way. Not\n>>>> every platform that has ssh-keygen conforms to the OpenSSH UI or output - a particular annoyance I get daily.\n>>>>\n>>\n>> What about a configurable command, like GIT_SSH_COMMAND to allow someone to plug in a mechanism or write something that\n>supplies a result you can handle? That's something I could probably work out on my own platforms.\n>>\n>\n>This is already possible by setting gpg.ssh.program (although you'd have to pass the sign operation as well)\n\nIs there documentation on the possible arguments the patch series will use for this so one can create a wrapper script? I had to look into the code to find out what GIT_SSH_COMMAND actually required when the ssh variant was \"ssh\". I'd rather not have to do that in this case.\n\nThanks,\nRandall\n\n"},{"id":"431541","messageId":"63cc209f-1111-9d03-f6ff-24598d7c9918@gigacodes.de","threadId":"56054","inReplyTo":"YQL8+UFtVJPlJroe@google.com","subject":"Re: [PATCH v6 6/9] ssh signing: add test prereqs","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-30T07:32:58Z","receivedAt":"2021-07-30T07:33:08Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 29.07.21 21:09, Josh Steadmon wrote:\n> On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:\n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>   \n>> +test_lazy_prereq GPGSSH '\n>> +\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n>> +\ttest $? != 127 || exit 1\n>> +\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n>> +\ttest $? = 0 || exit 1;\n>> +\tmkdir -p \"${GNUPGHOME}\" &&\n>> +\tchmod 0700 \"${GNUPGHOME}\" &&\n>> +\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n>> +\techo \"\\\"principal with number 1\\\" $(cat \"${GNUPGHOME}/ed25519_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n>> +\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n>> +\techo \"\\\"principal with number 2\\\" $(cat \"${GNUPGHOME}/rsa_2048_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n>> +\tssh-keygen -t ed25519 -N \"super_secret\" -C \"git ed25519 encrypted key\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n>> +\techo \"\\\"principal with number 3\\\" $(cat \"${GNUPGHOME}/protected_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n>> +\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n>> +\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n>> +'\n>> +\n>> +SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n>> +SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n>> +SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n>> +SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n>> +SIGNING_KEY_PASSPHRASE=\"super_secret\"\n>> +SIGNING_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n>> +\n>> +GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n>> +GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n>> +KEY_NOT_TRUSTED=\"No principal matched\"\n>> +BAD_SIGNATURE=\"Signature verification failed\"\n>> +\n> \n> Is there a reason why we don't use these variables in the script above?\n> \n> Also, in general I feel that it's better to add tests in the same commit\n> where new features are added, rather than having standalone test\n> commits.\n> \n\nIntially i wanted to fill them in the prereq but couldn't acces them in \nthe tests then.\n\nThanks, i have moved the variables above the prereq and used them there \nas well. makes sense.\nAlso i have prefixed them now with GPGSSH so we don't collide with any \nother tests accidentally.\n"},{"id":"431542","messageId":"30489b9f-8bbb-22c3-bd36-95f430a45ba9@gigacodes.de","threadId":"56054","inReplyTo":"03a101d784c9$0cb413a0$261c3ae0$@nexbridge.com","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-30T08:17:16Z","receivedAt":"2021-07-30T08:17:23Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 30.07.21 00:28, Randall S. Becker wrote:\n> On July 29, 2021 5:29 PM, Fabian Stelzer wrote:\n>> On 29.07.21 23:25, Randall S. Becker wrote:\n>>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote:\n>>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and\n>>>> verify signatures\n>>>>\n>>>> On 29.07.21 23:01, Randall S. Becker wrote:\n>>>>> On July 29, 2021 4:46 PM, Junio wrote:\n>>>>>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>>>>>\n>>>>>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>>>>\n>>>>>>>> Also, is this output documented to be stable even across locales?\n>>>>>>> Not really :/ (it currently is not locale specific)\n>>>>>>\n>>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>>>>>\n>>>>>>> The documentation states to only check the commands exit code. Do\n>>>>>>> we trust the exit code enough to rely on it for verification?\n>>>>>>\n>>>>>> Is the exit code sufficient to learn who signed it?  Without\n>>>>>> knowing that, we cannot see if the principal is in or not in our\n>>>>> keychain, no?\n>>>>>\n>>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>>>>>\n>>>>\n>>>> To find the principal (who signed it) we don't have to parse the output.\n>>>> Since verification is first a call to look up the principals matching\n>>>> the signatures public key from the allowedSignersFile and then trying\n>>>> verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH\n>> CA).\n>>>> Of course this even more relies on the exit code of ssh-keygen.\n>>>>\n>>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both.\n>>>\n>>> What about a configurable exit code for this? See the comment below about that.\n>>>\n>>\n>> I'm not sure what you mean. Something like \"treat exit(123) as success\"?\n> \n> How about gpg.ssh.successExit=123 or something like that.\n>\n\nI don't quite understand what the benefit would be. Do you have any \nspecific portability problems/concerns where the ssh-keygen format is \ndifferent or exit codes differ?\nI think using a script that provides exit(0) on success and the correct \noutput to wrap ssh-keygen and setting it in gpg.ssh.command can already \ncover edge cases when needed.\n\n> \n> Is there documentation on the possible arguments the patch series will use for this so one can create a wrapper script? I had to look into the code to find out what GIT_SSH_COMMAND actually required when the ssh variant was \"ssh\". I'd rather not have to do that in this case.\n> \n\nThe documentation in ssh-keygen(1) is quite good and straight forward \nfor verification and signing. Again if you have any specific portability \nconcerns i'd be glad to help.\n"},{"id":"431574","messageId":"001601d7854e$e0d24960$a276dc20$@nexbridge.com","threadId":"56054","inReplyTo":"30489b9f-8bbb-22c3-bd36-95f430a45ba9@gigacodes.de","subject":"RE: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2021-07-30T14:26:18Z","receivedAt":"2021-07-30T14:26:31Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On July 30, 2021 4:17 AM, Fabian Stelzer wrote:\n>Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures\n>\n>On 30.07.21 00:28, Randall S. Becker wrote:\n>> On July 29, 2021 5:29 PM, Fabian Stelzer wrote:\n>>> On 29.07.21 23:25, Randall S. Becker wrote:\n>>>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote:\n>>>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output\n>>>>> and verify signatures\n>>>>>\n>>>>> On 29.07.21 23:01, Randall S. Becker wrote:\n>>>>>> On July 29, 2021 4:46 PM, Junio wrote:\n>>>>>>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>>>>>>\n>>>>>>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>>>>>\n>>>>>>>>> Also, is this output documented to be stable even across locales?\n>>>>>>>> Not really :/ (it currently is not locale specific)\n>>>>>>>\n>>>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>>>>>>\n>>>>>>>> The documentation states to only check the commands exit code.\n>>>>>>>> Do we trust the exit code enough to rely on it for verification?\n>>>>>>>\n>>>>>>> Is the exit code sufficient to learn who signed it?  Without\n>>>>>>> knowing that, we cannot see if the principal is in or not in our\n>>>>>> keychain, no?\n>>>>>>\n>>>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>>>>>>\n>>>>>\n>>>>> To find the principal (who signed it) we don't have to parse the output.\n>>>>> Since verification is first a call to look up the principals\n>>>>> matching the signatures public key from the allowedSignersFile and\n>>>>> then trying verification with each one we already know which one\n>>>>> matched (usually there is only one. I think multiples is only\n>>>>> possible with an SSH\n>>> CA).\n>>>>> Of course this even more relies on the exit code of ssh-keygen.\n>>>>>\n>>>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both.\n>>>>\n>>>> What about a configurable exit code for this? See the comment below about that.\n>>>>\n>>>\n>>> I'm not sure what you mean. Something like \"treat exit(123) as success\"?\n>>\n>> How about gpg.ssh.successExit=123 or something like that.\n>>\n>\n>I don't quite understand what the benefit would be. Do you have any specific portability problems/concerns where the ssh-keygen format\n>is different or exit codes differ?\n>I think using a script that provides exit(0) on success and the correct output to wrap ssh-keygen and setting it in gpg.ssh.command can\n>already cover edge cases when needed.\n>\n>>\n>> Is there documentation on the possible arguments the patch series will use for this so one can create a wrapper script? I had to look into\n>the code to find out what GIT_SSH_COMMAND actually required when the ssh variant was \"ssh\". I'd rather not have to do that in this case.\n>>\n>\n>The documentation in ssh-keygen(1) is quite good and straight forward for verification and signing. Again if you have any specific\n>portability concerns i'd be glad to help.\n\nI do know the ssh-keygen interface and that does not really answer my doubts.\n\nMy point here is that ssh-keygen is not always available in the same form on all platforms. Providing a full emulation of all arguments is not effective or likely even possible, and a waste of time. I'm asking for documentation on what specific options you are using for each function. OpenSSL is not available everywhere, and even where it is, the latest versions are not always available. It is important to know what the specific interface is being used.\n\n\n"},{"id":"431575","messageId":"6f5f654c-fd5f-a8a5-acdc-14e24f6843c6@gigacodes.de","threadId":"56054","inReplyTo":"001601d7854e$e0d24960$a276dc20$@nexbridge.com","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-07-30T14:32:12Z","receivedAt":"2021-07-30T14:32:18Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\n\nOn 30.07.21 16:26, Randall S. Becker wrote:\n> On July 30, 2021 4:17 AM, Fabian Stelzer wrote:\n>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures\n>>\n>> On 30.07.21 00:28, Randall S. Becker wrote:\n>>> On July 29, 2021 5:29 PM, Fabian Stelzer wrote:\n>>>> On 29.07.21 23:25, Randall S. Becker wrote:\n>>>>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote:\n>>>>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output\n>>>>>> and verify signatures\n>>>>>>\n>>>>>> On 29.07.21 23:01, Randall S. Becker wrote:\n>>>>>>> On July 29, 2021 4:46 PM, Junio wrote:\n>>>>>>>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>>>>>>>\n>>>>>>>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>>>>>>\n>>>>>>>>>> Also, is this output documented to be stable even across locales?\n>>>>>>>>> Not really :/ (it currently is not locale specific)\n>>>>>>>>\n>>>>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>>>>>>>\n>>>>>>>>> The documentation states to only check the commands exit code.\n>>>>>>>>> Do we trust the exit code enough to rely on it for verification?\n>>>>>>>>\n>>>>>>>> Is the exit code sufficient to learn who signed it?  Without\n>>>>>>>> knowing that, we cannot see if the principal is in or not in our\n>>>>>>> keychain, no?\n>>>>>>>\n>>>>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>>>>>>>\n>>>>>>\n>>>>>> To find the principal (who signed it) we don't have to parse the output.\n>>>>>> Since verification is first a call to look up the principals\n>>>>>> matching the signatures public key from the allowedSignersFile and\n>>>>>> then trying verification with each one we already know which one\n>>>>>> matched (usually there is only one. I think multiples is only\n>>>>>> possible with an SSH\n>>>> CA).\n>>>>>> Of course this even more relies on the exit code of ssh-keygen.\n>>>>>>\n>>>>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both.\n>>>>>\n>>>>> What about a configurable exit code for this? See the comment below about that.\n>>>>>\n>>>>\n>>>> I'm not sure what you mean. Something like \"treat exit(123) as success\"?\n>>>\n>>> How about gpg.ssh.successExit=123 or something like that.\n>>>\n>>\n>> I don't quite understand what the benefit would be. Do you have any specific portability problems/concerns where the ssh-keygen format\n>> is different or exit codes differ?\n>> I think using a script that provides exit(0) on success and the correct output to wrap ssh-keygen and setting it in gpg.ssh.command can\n>> already cover edge cases when needed.\n>>\n>>>\n>>> Is there documentation on the possible arguments the patch series will use for this so one can create a wrapper script? I had to look into\n>> the code to find out what GIT_SSH_COMMAND actually required when the ssh variant was \"ssh\". I'd rather not have to do that in this case.\n>>>\n>>\n>> The documentation in ssh-keygen(1) is quite good and straight forward for verification and signing. Again if you have any specific\n>> portability concerns i'd be glad to help.\n> \n> I do know the ssh-keygen interface and that does not really answer my doubts.\n> \n> My point here is that ssh-keygen is not always available in the same form on all platforms. Providing a full emulation of all arguments is not effective or likely even possible, and a waste of time. I'm asking for documentation on what specific options you are using for each function. OpenSSL is not available everywhere, and even where it is, the latest versions are not always available. It is important to know what the specific interface is being used.\n> \n> \n\nFair enough. Where would you expect to look for such documentation?\nI'm not sure sth like config/gpg.txt is the right place for this.\n"},{"id":"431578","messageId":"001a01d78554$688e1cd0$39aa5670$@nexbridge.com","threadId":"56054","inReplyTo":"6f5f654c-fd5f-a8a5-acdc-14e24f6843c6@gigacodes.de","subject":"RE: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2021-07-30T15:05:54Z","receivedAt":"2021-07-30T15:06:05Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On July 30, 2021 10:32 AM, Fabian Stelzer wrote:\n>On 30.07.21 16:26, Randall S. Becker wrote:\n>> On July 30, 2021 4:17 AM, Fabian Stelzer wrote:\n>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and\n>>> verify signatures\n>>>\n>>> On 30.07.21 00:28, Randall S. Becker wrote:\n>>>> On July 29, 2021 5:29 PM, Fabian Stelzer wrote:\n>>>>> On 29.07.21 23:25, Randall S. Becker wrote:\n>>>>>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote:\n>>>>>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output\n>>>>>>> and verify signatures\n>>>>>>>\n>>>>>>> On 29.07.21 23:01, Randall S. Becker wrote:\n>>>>>>>> On July 29, 2021 4:46 PM, Junio wrote:\n>>>>>>>>> Fabian Stelzer <fs@gigacodes.de> writes:\n>>>>>>>>>\n>>>>>>>>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>>>>>>>\n>>>>>>>>>>> Also, is this output documented to be stable even across locales?\n>>>>>>>>>> Not really :/ (it currently is not locale specific)\n>>>>>>>>>\n>>>>>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless.\n>>>>>>>>>\n>>>>>>>>>> The documentation states to only check the commands exit code.\n>>>>>>>>>> Do we trust the exit code enough to rely on it for verification?\n>>>>>>>>>\n>>>>>>>>> Is the exit code sufficient to learn who signed it?  Without\n>>>>>>>>> knowing that, we cannot see if the principal is in or not in\n>>>>>>>>> our\n>>>>>>>> keychain, no?\n>>>>>>>>\n>>>>>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable.\n>>>>>>>>\n>>>>>>>\n>>>>>>> To find the principal (who signed it) we don't have to parse the output.\n>>>>>>> Since verification is first a call to look up the principals\n>>>>>>> matching the signatures public key from the allowedSignersFile\n>>>>>>> and then trying verification with each one we already know which\n>>>>>>> one matched (usually there is only one. I think multiples is only\n>>>>>>> possible with an SSH\n>>>>> CA).\n>>>>>>> Of course this even more relies on the exit code of ssh-keygen.\n>>>>>>>\n>>>>>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both.\n>>>>>>\n>>>>>> What about a configurable exit code for this? See the comment below about that.\n>>>>>>\n>>>>>\n>>>>> I'm not sure what you mean. Something like \"treat exit(123) as success\"?\n>>>>\n>>>> How about gpg.ssh.successExit=123 or something like that.\n>>>>\n>>>\n>>> I don't quite understand what the benefit would be. Do you have any\n>>> specific portability problems/concerns where the ssh-keygen format is different or exit codes differ?\n>>> I think using a script that provides exit(0) on success and the\n>>> correct output to wrap ssh-keygen and setting it in gpg.ssh.command can already cover edge cases when needed.\n>>>\n>>>>\n>>>> Is there documentation on the possible arguments the patch series\n>>>> will use for this so one can create a wrapper script? I had to look\n>>>> into\n>>> the code to find out what GIT_SSH_COMMAND actually required when the ssh variant was \"ssh\". I'd rather not have to do that in this\n>case.\n>>>>\n>>>\n>>> The documentation in ssh-keygen(1) is quite good and straight forward\n>>> for verification and signing. Again if you have any specific portability concerns i'd be glad to help.\n>>\n>> I do know the ssh-keygen interface and that does not really answer my doubts.\n>>\n>> My point here is that ssh-keygen is not always available in the same form on all platforms. Providing a full emulation of all arguments is\n>not effective or likely even possible, and a waste of time. I'm asking for documentation on what specific options you are using for each\n>function. OpenSSL is not available everywhere, and even where it is, the latest versions are not always available. It is important to know\n>what the specific interface is being used.\n>>\n>>\n>\n>Fair enough. Where would you expect to look for such documentation?\n>I'm not sure sth like config/gpg.txt is the right place for this.\n\nMy suggestion is wherever gpg.ssh.command is documented. So really, I think config/gpg.txt is the place. It's that or we create some common location for compatibility layer documentation (what I would really prefer). If there is a good place to put that, I might be willing to take on the documentation task, but my $DAYJOB is keeping me from anything heavy at this point.\n\nWith my thanks,\nRandall\n\n"},{"id":"431802","messageId":"91adff99-5f56-643d-e328-472256dc60a1@gigacodes.de","threadId":"56054","inReplyTo":"76cc0f7d-90d9-18bf-e749-feff8e584453@gigacodes.de","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-08-03T07:43:18Z","receivedAt":"2021-08-03T07:43:29Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\n\nOn 29.07.21 15:52, Fabian Stelzer wrote:\n> On 29.07.21 11:48, Fabian Stelzer wrote:\n>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>> to verify a ssh signature we first call ssh-keygen -Y find-principal to\n>>>> look up the signing principal by their public key from the\n>>>> allowedSignersFile. If the key is found then we do a verify. Otherwise\n>>>> we only validate the signature but can not verify the signers identity.\n>>>\n>>> Is this the same behavior as GPG signing in Git?\n>>\n>> Not quite. GPG requires every signers public key to be in the keyring. \n>> But even then, the \"UNDEFINED\" Trust level is enough to be valid for \n>> commits (but not for merges).\n>> For SSH i did set the unknown keys to UNDEFINED as well and they will \n>> show up as valid but not have a principal to identify them.\n>> This way a project can decide wether to accept unknown keys by setting \n>> the gpg.mintrustlevel. So the default behaviour is different.\n>> The alternative would be to treat unknown keys always as invalid.\n>>\n> \n> I thought a bit more about this and my approach is indeed problematic \n> especially when a repo has both gpg and ssh signatures. The trust level \n> setting can then not behave differently for both.\n> \n> My intention of still showing valid but unknown signatures in the log as \n> ok (but unknown) was to encourage users to always sign their work even \n> if they are not (yet) trusted in the allowedSignersFile.\n> \n> I think the way forward should be to treat unknown singing keys as not \n> verified like gpg does.\n> \n> If a ssh key is verified and in the allowedSignersFile i would still set \n> its trust level to \"FULLY\".\n\ni dug a bit deeper into the gpg code/tests and it actually already \nbehaves the same. untrusted signatures still return successfull on a \nverify-commit/tag even if the key is completely untrusted. my patch does \nthe same thing for ssh signatures. i'll send a new revision later today \nwith all the other fixes.\n"},{"id":"431808","messageId":"46c2a269-132d-3209-804c-c7d4c80ea083@gigacodes.de","threadId":"56054","inReplyTo":"91adff99-5f56-643d-e328-472256dc60a1@gigacodes.de","subject":"Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-08-03T09:33:23Z","receivedAt":"2021-08-03T09:33:31Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\n\nOn 03.08.21 09:43, Fabian Stelzer wrote:\n> \n> \n> On 29.07.21 15:52, Fabian Stelzer wrote:\n>> On 29.07.21 11:48, Fabian Stelzer wrote:\n>>> On 29.07.21 01:04, Jonathan Tan wrote:\n>>>>> to verify a ssh signature we first call ssh-keygen -Y \n>>>>> find-principal to\n>>>>> look up the signing principal by their public key from the\n>>>>> allowedSignersFile. If the key is found then we do a verify. Otherwise\n>>>>> we only validate the signature but can not verify the signers \n>>>>> identity.\n>>>>\n>>>> Is this the same behavior as GPG signing in Git?\n>>>\n>>> Not quite. GPG requires every signers public key to be in the \n>>> keyring. But even then, the \"UNDEFINED\" Trust level is enough to be \n>>> valid for commits (but not for merges).\n>>> For SSH i did set the unknown keys to UNDEFINED as well and they will \n>>> show up as valid but not have a principal to identify them.\n>>> This way a project can decide wether to accept unknown keys by \n>>> setting the gpg.mintrustlevel. So the default behaviour is different.\n>>> The alternative would be to treat unknown keys always as invalid.\n>>>\n>>\n>> I thought a bit more about this and my approach is indeed problematic \n>> especially when a repo has both gpg and ssh signatures. The trust \n>> level setting can then not behave differently for both.\n>>\n>> My intention of still showing valid but unknown signatures in the log \n>> as ok (but unknown) was to encourage users to always sign their work \n>> even if they are not (yet) trusted in the allowedSignersFile.\n>>\n>> I think the way forward should be to treat unknown singing keys as not \n>> verified like gpg does.\n>>\n>> If a ssh key is verified and in the allowedSignersFile i would still \n>> set its trust level to \"FULLY\".\n> \n> i dug a bit deeper into the gpg code/tests and it actually already \n> behaves the same. untrusted signatures still return successfull on a \n> verify-commit/tag even if the key is completely untrusted. my patch does \n> the same thing for ssh signatures. i'll send a new revision later today \n> with all the other fixes.\n\noh boy... sorry for all the emails. the gpg stuff can be really \nconfusing. especially since there's different meanings of \"untrusted\", \n\"unknown\" and \"undefined\" depending on which docs/codebase you look \ninto. Especially \"untrusted\" is not really a gpg term but used in the \ncodebase in tests like 'verify-commit exits success on untrusted \nsignature' which tests for a key already in the keyring but not with any \nspecified trust level. I could not actually find any gpg test for a \nsignature that is completely unknown. (i will add one)\n\nGPG does a successful verify-commit/tag on keys that are \"known\". \nMeaning that to be marked as good signatures all you need is to have the \npublic key in your keyring. This key can still have an unknown/undefined \ntrust level (meaning its in the keyring but no decision on trust has \nbeen made). A key thats not in the keyring has no trustlevel or anything \nbut fails hard with \"no public key\".\n\nSSH signing does not really make this distinction. A key is either in \nthe allowedSigners file (and therefore trusted), completely unknown, or \nrevoked via the revokedSigners file.\nTo make this behave like gpg does i will make verification fail on \ncompletely unknown keys. There is no use of the undefined trust level \nfor ssh then and i will set keys in the allowedSigners file to fully \ntrusted so they will be accepted for merges as well. I don't see any way \nto have keys that are valid for commits but not merge with ssh then but \nthat should be the only difference to gpg.\n"},{"id":"431812","messageId":"91fd0159e1f8d729c9144f61109496f018dcef47.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:50Z","receivedAt":"2021-08-03T13:46:06Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nOpenssh v8.2p1 added some new options to ssh-keygen for signature\ncreation and verification. These allow us to use ssh keys for git\nsignatures easily.\n\nIn our corporate environment we use PIV x509 Certs on Yubikeys for email\nsigning/encryption and ssh keys which I think is quite common\n(at least for the email part). This way we can establish the correct\ntrust for the SSH Keys without setting up a separate GPG Infrastructure\n(which is still quite painful for users) or implementing x509 signing\nsupport for git (which lacks good forwarding mechanisms).\nUsing ssh agent forwarding makes this feature easily usable in todays\ndevelopment environments where code is often checked out in remote VMs / containers.\nIn such a setup the keyring & revocationKeyring can be centrally\ngenerated from the x509 CA information and distributed to the users.\n\nTo be able to implement new signing formats this commit:\n - makes the sigc structure more generic by renaming \"gpg_output\" to\n   \"output\"\n - introduces function pointers in the gpg_format structure to call\n   format specific signing and verification functions\n - moves format detection from verify_signed_buffer into the check_signature\n   api function and calls the format specific verify\n - renames and wraps sign_buffer to handle format specific signing logic\n   as well\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n fmt-merge-msg.c |   6 +--\n gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------\n gpg-interface.h |   2 +-\n log-tree.c      |   8 ++--\n pretty.c        |   4 +-\n 5 files changed, 74 insertions(+), 50 deletions(-)\n\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex 0f66818e0f8..fb300bb4b67 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -526,11 +526,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tbuf = payload.buf;\n \t\t\tlen = payload.len;\n \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n-\t\t\t\t\t sig.len, &sigc) &&\n-\t\t\t\t!sigc.gpg_output)\n+\t\t\t\t\t    sig.len, &sigc) &&\n+\t\t\t    !sigc.output)\n \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n \t\t\telse\n-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n \t\t}\n \t\tsignature_check_clear(&sigc);\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..db54b054162 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -15,6 +15,12 @@ struct gpg_format {\n \tconst char *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n+\tint (*verify_signed_buffer)(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n+\n static struct gpg_format gpg_format[] = {\n-\t{ .name = \"openpgp\", .program = \"gpg\",\n-\t  .verify_args = openpgp_verify_args,\n-\t  .sigs = openpgp_sigs\n+\t{\n+\t\t.name = \"openpgp\",\n+\t\t.program = \"gpg\",\n+\t\t.verify_args = openpgp_verify_args,\n+\t\t.sigs = openpgp_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n-\t{ .name = \"x509\", .program = \"gpgsm\",\n-\t  .verify_args = x509_verify_args,\n-\t  .sigs = x509_sigs\n+\t{\n+\t\t.name = \"x509\",\n+\t\t.program = \"gpgsm\",\n+\t\t.verify_args = x509_verify_args,\n+\t\t.sigs = x509_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n };\n \n@@ -72,7 +93,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\n-\tFREE_AND_NULL(sigc->gpg_output);\n+\tFREE_AND_NULL(sigc->output);\n \tFREE_AND_NULL(sigc->gpg_status);\n \tFREE_AND_NULL(sigc->signer);\n \tFREE_AND_NULL(sigc->key);\n@@ -257,16 +278,16 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n-static int verify_signed_buffer(const char *payload, size_t payload_size,\n-\t\t\t\tconst char *signature, size_t signature_size,\n-\t\t\t\tstruct strbuf *gpg_output,\n-\t\t\t\tstruct strbuf *gpg_status)\n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n-\tstruct gpg_format *fmt;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf gpg_stdout = STRBUF_INIT;\n+\tstruct strbuf gpg_stderr = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -279,10 +300,6 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\treturn -1;\n \t}\n \n-\tfmt = get_format_by_sig(signature);\n-\tif (!fmt)\n-\t\tBUG(\"bad signature '%s'\", signature);\n-\n \tstrvec_push(&gpg.args, fmt->program);\n \tstrvec_pushv(&gpg.args, fmt->verify_args);\n \tstrvec_pushl(&gpg.args,\n@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\t     \"--verify\", temp->filename.buf, \"-\",\n \t\t     NULL);\n \n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n-\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n+\tret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,\n+\t\t\t   &gpg_stderr, 0);\n \tsigchain_pop(SIGPIPE);\n \n \tdelete_tempfile(&temp);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n-\tstrbuf_release(&buf); /* no matter it was used or not */\n+\tret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tsigc->output = strbuf_detach(&gpg_stderr, NULL);\n+\tsigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);\n+\n+\tparse_gpg_output(sigc);\n+\n+\tstrbuf_release(&gpg_stdout);\n+\tstrbuf_release(&gpg_stderr);\n \n \treturn ret;\n }\n@@ -309,35 +330,32 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n-\tstruct strbuf gpg_output = STRBUF_INIT;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct gpg_format *fmt;\n \tint status;\n \n \tsigc->result = 'N';\n \tsigc->trust_level = -1;\n \n-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n-\t\t\t\t      &gpg_output, &gpg_status);\n-\tif (status && !gpg_output.len)\n-\t\tgoto out;\n-\tsigc->payload = xmemdupz(payload, plen);\n-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tfmt = get_format_by_sig(signature);\n+\tif (!fmt)\n+\t\tdie(_(\"bad/incompatible signature '%s'\"), signature);\n+\n+\tstatus = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,\n+\t\t\t\t\t   slen);\n+\n+\tif (status && !sigc->output)\n+\t\treturn !!status;\n+\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n- out:\n-\tstrbuf_release(&gpg_status);\n-\tstrbuf_release(&gpg_output);\n-\n \treturn !!status;\n }\n \n void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n {\n-\tconst char *output = flags & GPG_VERIFY_RAW ?\n-\t\tsigc->gpg_status : sigc->gpg_output;\n+\tconst char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :\n+\t\t\t\t\t\t\t    sigc->output;\n \n \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n \t\tfputs(sigc->payload, stdout);\n@@ -441,6 +459,12 @@ const char *get_signing_key(void)\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n+{\n+\treturn use_format->sign_buffer(buffer, signature, signing_key);\n+}\n+\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t  const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..feac4decf8b 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -17,7 +17,7 @@ enum signature_trust_level {\n \n struct signature_check {\n \tchar *payload;\n-\tchar *gpg_output;\n+\tchar *output;\n \tchar *gpg_status;\n \n \t/*\ndiff --git a/log-tree.c b/log-tree.c\nindex 6dc4412268b..644893fd8cf 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -515,10 +515,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit)\n \n \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n \t\t\t\t signature.len, &sigc);\n-\tif (status && !sigc.gpg_output)\n+\tif (status && !sigc.output)\n \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n \telse\n-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n+\t\tshow_sig_lines(opt, status, sigc.output);\n \tsignature_check_clear(&sigc);\n \n  out:\n@@ -585,8 +585,8 @@ static int show_one_mergetag(struct commit *commit,\n \t\t/* could have a good signature */\n \t\tstatus = check_signature(payload.buf, payload.len,\n \t\t\t\t\t signature.buf, signature.len, &sigc);\n-\t\tif (sigc.gpg_output)\n-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n+\t\tif (sigc.output)\n+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n \t\telse\n \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n \t\tsignature_check_clear(&sigc);\ndiff --git a/pretty.c b/pretty.c\nindex 9631529c10a..be477bd51f2 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n \t\tswitch (placeholder[1]) {\n \t\tcase 'G':\n-\t\t\tif (c->signature_check.gpg_output)\n-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n+\t\t\tif (c->signature_check.output)\n+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n \t\t\tbreak;\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n-- \ngitgitgadget\n\n"},{"id":"431813","messageId":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v6.git.git.1627501009.gitgitgadget@gmail.com","subject":"[PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:49Z","receivedAt":"2021-08-03T13:46:07Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"openssh 8.7 will add valid-after, valid-before options to the allowed keys\nkeyring. This allows us to pass the commit timestamp to the verification\ncall and make key rollover possible and still be able to verify older\ncommits. Set valid-after to the current date when adding your key to the\nkeyring and set valid-before to make it fail if used after a certain date.\nSoftware like gitolite/github or corporate automation can do this\nautomatically when ssh push keys are addded / removed I will add this\nfeature in a follow up patch afterwards.\n\nv3 addresses some issues & refactoring and splits the large commit into\nseveral smaller ones.\n\nv4:\n\n * restructures and cleans up the whole patch set - patches build on its own\n   now and commit messages try to explain whats going on\n * got rid of the if branches and used callback functions in the format\n   struct\n * fixed a bug with whitespace in principal identifiers that required a\n   rewrite of the parse_ssh_output function\n * rewrote documentation to be more clear - also renamed keyring back to\n   allowedSignersFile\n\nv5:\n\n * moved t7527 to t7528 to not collide with another patch in \"seen\"\n * clean up return logic for failed signing & verification\n * some minor renames / reformatting to make things clearer\n\nv6: fixed tests when using shm output dir\n\nv7:\n\n * change unknown signing key behavior to fail verify-commit/tag just like\n   gpg does\n * add test for unknown signing keys for ssh & gpg\n * made default signing key retrieval configurable\n   (gpg.ssh.defaultKeyCommand). We could default this to \"ssh-add -L\" but\n   would risk some users signing with a wrong key\n * die() instead of error in case of incompatible signatures to match\n   current BUG() behaviour more\n * various review fixes (early return for config parse, missing free,\n   comments)\n * got rid of strcmp(\"ssh\") branches and used format configurable callbacks\n   everywhere\n * moved documentation changes into the commits adding the specific\n   functionality\n\nThe test 'verify-commit verifies multiply signed commits' relies on the\ncommit/author date that was incremented via test_tick in the inital function\ndoing all the commits even though it creates its own. This should be reset\nor otherwise set to a known state. But I'm not sure how.\n\nFabian Stelzer (9):\n  ssh signing: preliminary refactoring and clean-up\n  ssh signing: add test prereqs\n  ssh signing: add ssh key format and signing code\n  ssh signing: retrieve a default key from ssh-agent\n  ssh signing: provide a textual signing_key_id\n  ssh signing: verify signatures using ssh-keygen\n  ssh signing: duplicate t7510 tests for commits\n  ssh signing: tests for logs, tags & push certs\n  ssh signing: test that gpg fails for unkown keys\n\n Documentation/config/gpg.txt     |  45 ++-\n Documentation/config/user.txt    |   7 +\n builtin/receive-pack.c           |   4 +\n fmt-merge-msg.c                  |   6 +-\n gpg-interface.c                  | 571 ++++++++++++++++++++++++++++---\n gpg-interface.h                  |   8 +-\n log-tree.c                       |   8 +-\n pretty.c                         |   4 +-\n send-pack.c                      |   8 +-\n t/lib-gpg.sh                     |  28 ++\n t/t4202-log.sh                   |  23 ++\n t/t5534-push-signed.sh           | 101 ++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++\n t/t7510-signed-commit.sh         |  29 +-\n t/t7528-signed-commit-ssh.sh     | 398 +++++++++++++++++++++\n 15 files changed, 1335 insertions(+), 66 deletions(-)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\n\nbase-commit: 940fe202adcbf9fa1825c648d97cbe1b90d26aec\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v7\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v7\nPull-Request: https://github.com/git/git/pull/1041\n\nRange-diff vs v6:\n\n  1:  7c8502c65b8 !  1:  91fd0159e1f ssh signing: preliminary refactoring and clean-up\n     @@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay\n      -\tparse_gpg_output(sigc);\n      +\tfmt = get_format_by_sig(signature);\n      +\tif (!fmt)\n     -+\t\treturn error(_(\"bad/incompatible signature '%s'\"), signature);\n     ++\t\tdie(_(\"bad/incompatible signature '%s'\"), signature);\n      +\n      +\tstatus = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,\n      +\t\t\t\t\t   slen);\n     @@ gpg-interface.c: const char *get_signing_key(void)\n      +}\n      +\n      +static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     -+\t\t    const char *signing_key)\n     ++\t\t\t  const char *signing_key)\n       {\n       \tstruct child_process gpg = CHILD_PROCESS_INIT;\n       \tint ret;\n  6:  18a26ca49e7 !  2:  fe98052a3ea ssh signing: add test prereqs\n     @@ Metadata\n       ## Commit message ##\n          ssh signing: add test prereqs\n      \n     -    generate some ssh keys and a allowedSignersFile for testing\n     +    Generate some ssh keys and a allowedSignersFile for testing\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '\n       \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n       '\n       \n     ++GPGSSH_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n     ++GPGSSH_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n     ++GPGSSH_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n     ++GPGSSH_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n     ++GPGSSH_KEY_PASSPHRASE=\"super_secret\"\n     ++GPGSSH_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n     ++\n     ++GPGSSH_GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n     ++GPGSSH_GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n     ++GPGSSH_KEY_NOT_TRUSTED=\"No principal matched\"\n     ++GPGSSH_BAD_SIGNATURE=\"Signature verification failed\"\n     ++\n      +test_lazy_prereq GPGSSH '\n      +\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n      +\ttest $? != 127 || exit 1\n     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '\n      +\ttest $? = 0 || exit 1;\n      +\tmkdir -p \"${GNUPGHOME}\" &&\n      +\tchmod 0700 \"${GNUPGHOME}\" &&\n     -+\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GNUPGHOME}/ed25519_ssh_signing_key\" >/dev/null &&\n     -+\techo \"\\\"principal with number 1\\\" $(cat \"${GNUPGHOME}/ed25519_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     -+\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GNUPGHOME}/rsa_2048_ssh_signing_key\" >/dev/null &&\n     -+\techo \"\\\"principal with number 2\\\" $(cat \"${GNUPGHOME}/rsa_2048_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     -+\tssh-keygen -t ed25519 -N \"super_secret\" -C \"git ed25519 encrypted key\" -f \"${GNUPGHOME}/protected_ssh_signing_key\" >/dev/null &&\n     -+\techo \"\\\"principal with number 3\\\" $(cat \"${GNUPGHOME}/protected_ssh_signing_key.pub\")\" >> \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     -+\tcat \"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\" &&\n     -+\tssh-keygen -t ed25519 -N \"\" -f \"${GNUPGHOME}/untrusted_ssh_signing_key\" >/dev/null\n     ++\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GPGSSH_KEY_PRIMARY}\" >/dev/null &&\n     ++\techo \"\\\"principal with number 1\\\" $(cat \"${GPGSSH_KEY_PRIMARY}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n     ++\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GPGSSH_KEY_SECONDARY}\" >/dev/null &&\n     ++\techo \"\\\"principal with number 2\\\" $(cat \"${GPGSSH_KEY_SECONDARY}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n     ++\tssh-keygen -t ed25519 -N \"${GPGSSH_KEY_PASSPHRASE}\" -C \"git ed25519 encrypted key\" -f \"${GPGSSH_KEY_WITH_PASSPHRASE}\" >/dev/null &&\n     ++\techo \"\\\"principal with number 3\\\" $(cat \"${GPGSSH_KEY_WITH_PASSPHRASE}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n     ++\tssh-keygen -t ed25519 -N \"\" -f \"${GPGSSH_KEY_UNTRUSTED}\" >/dev/null\n      +'\n     -+\n     -+SIGNING_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n     -+SIGNING_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n     -+SIGNING_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n     -+SIGNING_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n     -+SIGNING_KEY_PASSPHRASE=\"super_secret\"\n     -+SIGNING_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n     -+\n     -+GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n     -+GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n     -+KEY_NOT_TRUSTED=\"No principal matched\"\n     -+BAD_SIGNATURE=\"Signature verification failed\"\n      +\n       sanitize_pgp() {\n       \tperl -ne '\n  2:  f05bab16096 !  3:  80d2d55d22e ssh signing: add ssh signature format and signing using ssh keys\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    ssh signing: add ssh signature format and signing using ssh keys\n     +    ssh signing: add ssh key format and signing code\n      \n     -    implements the actual sign_buffer_ssh operation and move some shared\n     +    Implements the actual sign_buffer_ssh operation and move some shared\n          cleanup code into a strbuf function\n      \n          Set gpg.format = ssh and user.signingkey to either a ssh public key\n     @@ Commit message\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     + ## Documentation/config/gpg.txt ##\n     +@@ Documentation/config/gpg.txt: gpg.program::\n     + \n     + gpg.format::\n     + \tSpecifies which key format to use when signing with `--gpg-sign`.\n     +-\tDefault is \"openpgp\" and another possible value is \"x509\".\n     ++\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n     + \n     + gpg.<format>.program::\n     + \tUse this to customize the program used for the signing format you\n     + \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n     + \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n     +-\tvalue for `gpg.x509.program` is \"gpgsm\".\n     ++\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n     + \n     + gpg.minTrustLevel::\n     + \tSpecifies a minimum trust level for signature verification.  If\n     +\n     + ## Documentation/config/user.txt ##\n     +@@ Documentation/config/user.txt: user.signingKey::\n     + \tcommit, you can override the default selection with this variable.\n     + \tThis option is passed unchanged to gpg's --local-user parameter,\n     + \tso you may specify a key using any method that gpg supports.\n     ++\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n     ++\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n     ++\tcorresponds to the private key used for signing. The private key\n     ++\tneeds to be available via ssh-agent. Alternatively it can be set to\n     ++\ta file containing a private key directly.\n     +\n       ## gpg-interface.c ##\n      @@ gpg-interface.c: static const char *x509_sigs[] = {\n       \tNULL\n     @@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature\n      +}\n      +\n       static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     - \t\t    const char *signing_key)\n     + \t\t\t  const char *signing_key)\n       {\n       \tstruct child_process gpg = CHILD_PROCESS_INIT;\n       \tint ret;\n     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf\n      +\t\t\t\t    key_file->filename.buf);\n      +\t\t\tgoto out;\n      +\t\t}\n     -+\t\tssh_signing_key_file = key_file->filename.buf;\n     ++\t\tssh_signing_key_file = strbuf_detach(&key_file->filename, NULL);\n      +\t} else {\n      +\t\t/* We assume a file */\n      +\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf\n      +\t\tdelete_tempfile(&buffer_file);\n      +\tstrbuf_release(&signer_stderr);\n      +\tstrbuf_release(&ssh_signature_filename);\n     ++\tFREE_AND_NULL(ssh_signing_key_file);\n      +\treturn ret;\n      +}\n  3:  071e6173d8e !  4:  83ece42e1de ssh signing: retrieve a default key from ssh-agent\n     @@ Metadata\n       ## Commit message ##\n          ssh signing: retrieve a default key from ssh-agent\n      \n     -    if user.signingkey is not set and a ssh signature is requested we call\n     -    ssh-add -L and use the first key we get\n     +    If user.signingkey is not set and a ssh signature is requested we call\n     +    gpg.ssh.defaultKeyCommand (typically \"ssh-add -L\") and use the first key we get\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     + ## Documentation/config/gpg.txt ##\n     +@@ Documentation/config/gpg.txt: gpg.minTrustLevel::\n     + * `marginal`\n     + * `fully`\n     + * `ultimate`\n     ++\n     ++gpg.ssh.defaultKeyCommand:\n     ++\tThis command that will be run when user.signingkey is not set and a ssh\n     ++\tsignature is requested. On successful exit a valid ssh public key is\n     ++\texpected in the\tfirst line of its output. To automatically use the first\n     ++\tavailable key from your ssh-agent set this to \"ssh-add -L\".\n     +\n     + ## Documentation/config/user.txt ##\n     +@@ Documentation/config/user.txt: user.signingKey::\n     + \tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n     + \tcorresponds to the private key used for signing. The private key\n     + \tneeds to be available via ssh-agent. Alternatively it can be set to\n     +-\ta file containing a private key directly.\n     ++\ta file containing a private key directly. If not set git will call\n     ++\tgpg.ssh.defaultKeyCommand (e.g.: \"ssh-add -L\") and try to use the first\n     ++\tkey available.\n     +\n       ## gpg-interface.c ##\n     +@@\n     + #include \"gpg-interface.h\"\n     + #include \"sigchain.h\"\n     + #include \"tempfile.h\"\n     ++#include \"alias.h\"\n     + \n     + static char *configured_signing_key;\n     ++static const char *ssh_default_key_command;\n     + static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n     + \n     + struct gpg_format {\n     +@@ gpg-interface.c: struct gpg_format {\n     + \t\t\t\t    size_t signature_size);\n     + \tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n     + \t\t\t   const char *signing_key);\n     ++\tconst char *(*get_default_key)(void);\n     + };\n     + \n     + static const char *openpgp_verify_args[] = {\n     +@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n     + static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n     + \t\t\t   const char *signing_key);\n     + \n     ++static const char *get_default_ssh_signing_key(void);\n     ++\n     + static struct gpg_format gpg_format[] = {\n     + \t{\n     + \t\t.name = \"openpgp\",\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.sigs = openpgp_sigs,\n     + \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n     + \t\t.sign_buffer = sign_buffer_gpg,\n     ++\t\t.get_default_key = NULL,\n     + \t},\n     + \t{\n     + \t\t.name = \"x509\",\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.sigs = x509_sigs,\n     + \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n     + \t\t.sign_buffer = sign_buffer_gpg,\n     ++\t\t.get_default_key = NULL,\n     + \t},\n     + \t{\n     + \t\t.name = \"ssh\",\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.verify_args = ssh_verify_args,\n     + \t\t.sigs = ssh_sigs,\n     + \t\t.verify_signed_buffer = NULL, /* TODO */\n     +-\t\t.sign_buffer = sign_buffer_ssh\n     ++\t\t.sign_buffer = sign_buffer_ssh,\n     ++\t\t.get_default_key = get_default_ssh_signing_key,\n     + \t},\n     + };\n     + \n     +@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     + \t\treturn 0;\n     + \t}\n     + \n     ++\tif (!strcmp(var, \"gpg.ssh.defaultkeycommand\")) {\n     ++\t\tif (!value)\n     ++\t\t\treturn config_error_nonbool(var);\n     ++\t\treturn git_config_string(&ssh_default_key_command, var, value);\n     ++\t}\n     ++\n     + \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n     + \t\tfmtname = \"openpgp\";\n     + \n      @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n       \treturn 0;\n       }\n       \n      +/* Returns the first public key from an ssh-agent to use for signing */\n     -+static char *get_default_ssh_signing_key(void)\n     ++static const char *get_default_ssh_signing_key(void)\n      +{\n     -+\tstruct child_process ssh_add = CHILD_PROCESS_INIT;\n     ++\tstruct child_process ssh_default_key = CHILD_PROCESS_INIT;\n      +\tint ret = -1;\n     -+\tstruct strbuf key_stdout = STRBUF_INIT;\n     ++\tstruct strbuf key_stdout = STRBUF_INIT, key_stderr = STRBUF_INIT;\n      +\tstruct strbuf **keys;\n     ++\tchar *key_command = NULL;\n     ++\tconst char **argv;\n     ++\tint n;\n     ++\tchar *default_key = NULL;\n     ++\n     ++\tif (!ssh_default_key_command)\n     ++\t\tdie(_(\"either user.signingkey or gpg.ssh.defaultKeyCommand needs to be configured\"));\n     ++\n     ++\tkey_command = xstrdup(ssh_default_key_command);\n     ++\tn = split_cmdline(key_command, &argv);\n     ++\n     ++\tif (n < 0)\n     ++\t\tdie(\"malformed build-time gpg.ssh.defaultKeyCommand: %s\",\n     ++\t\t    split_cmdline_strerror(n));\n     ++\n     ++\tstrvec_pushv(&ssh_default_key.args, argv);\n     ++\tret = pipe_command(&ssh_default_key, NULL, 0, &key_stdout, 0,\n     ++\t\t\t   &key_stderr, 0);\n      +\n     -+\tstrvec_pushl(&ssh_add.args, \"ssh-add\", \"-L\", NULL);\n     -+\tret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);\n      +\tif (!ret) {\n      +\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n     -+\t\tif (keys[0])\n     -+\t\t\treturn strbuf_detach(keys[0], NULL);\n     ++\t\tif (keys[0] && starts_with(keys[0]->buf, \"ssh-\")) {\n     ++\t\t\tdefault_key = strbuf_detach(keys[0], NULL);\n     ++\t\t} else {\n     ++\t\t\twarning(_(\"gpg.ssh.defaultKeycommand succeeded but returned no keys: %s %s\"),\n     ++\t\t\t\tkey_stderr.buf, key_stdout.buf);\n     ++\t\t}\n     ++\n     ++\t\tstrbuf_list_free(keys);\n     ++\t} else {\n     ++\t\twarning(_(\"gpg.ssh.defaultKeyCommand failed: %s %s\"),\n     ++\t\t\tkey_stderr.buf, key_stdout.buf);\n      +\t}\n      +\n     ++\tfree(key_command);\n     ++\tfree(argv);\n      +\tstrbuf_release(&key_stdout);\n     -+\treturn \"\";\n     ++\n     ++\treturn default_key;\n      +}\n      +\n       const char *get_signing_key(void)\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n       \tif (configured_signing_key)\n       \t\treturn configured_signing_key;\n      -\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\treturn get_default_ssh_signing_key();\n     -+\t} else {\n     -+\t\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n     ++\tif (use_format->get_default_key) {\n     ++\t\treturn use_format->get_default_key();\n      +\t}\n     ++\n     ++\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n       }\n       \n       int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n  4:  7d1d131ff5b !  5:  76bc9eb4079 ssh signing: provide a textual representation of the signing key\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    ssh signing: provide a textual representation of the signing key\n     +    ssh signing: provide a textual signing_key_id\n      \n     -    for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\n     -    in push certs and textual output we prefer the ssh fingerprint instead.\n     +    For ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\n     +    In push certs and textual output we prefer the ssh fingerprint instead.\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## gpg-interface.c ##\n     +@@ gpg-interface.c: struct gpg_format {\n     + \tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n     + \t\t\t   const char *signing_key);\n     + \tconst char *(*get_default_key)(void);\n     ++\tconst char *(*get_key_id)(void);\n     + };\n     + \n     + static const char *openpgp_verify_args[] = {\n     +@@ gpg-interface.c: static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n     + \n     + static const char *get_default_ssh_signing_key(void);\n     + \n     ++static const char *get_ssh_key_id(void);\n     ++\n     + static struct gpg_format gpg_format[] = {\n     + \t{\n     + \t\t.name = \"openpgp\",\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n     + \t\t.sign_buffer = sign_buffer_gpg,\n     + \t\t.get_default_key = NULL,\n     ++\t\t.get_key_id = NULL,\n     + \t},\n     + \t{\n     + \t\t.name = \"x509\",\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n     + \t\t.sign_buffer = sign_buffer_gpg,\n     + \t\t.get_default_key = NULL,\n     ++\t\t.get_key_id = NULL,\n     + \t},\n     + \t{\n     + \t\t.name = \"ssh\",\n     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n     + \t\t.verify_signed_buffer = NULL, /* TODO */\n     + \t\t.sign_buffer = sign_buffer_ssh,\n     + \t\t.get_default_key = get_default_ssh_signing_key,\n     ++\t\t.get_key_id = get_ssh_key_id,\n     + \t},\n     + };\n     + \n      @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n       \treturn 0;\n       }\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n      +\t * With SSH Signing this can contain a filename or a public key\n      +\t * For textual representation we usually want a fingerprint\n      +\t */\n     -+\tif (istarts_with(signing_key, \"ssh-\")) {\n     ++\tif (starts_with(signing_key, \"ssh-\")) {\n      +\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n      +\t\tret = pipe_command(&ssh_keygen, signing_key,\n      +\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n      +}\n      +\n       /* Returns the first public key from an ssh-agent to use for signing */\n     - static char *get_default_ssh_signing_key(void)\n     + static const char *get_default_ssh_signing_key(void)\n       {\n     -@@ gpg-interface.c: static char *get_default_ssh_signing_key(void)\n     - \treturn \"\";\n     +@@ gpg-interface.c: static const char *get_default_ssh_signing_key(void)\n     + \treturn default_key;\n       }\n       \n     -+/* Returns a textual but unique representation ot the signing key */\n     ++static const char *get_ssh_key_id(void) {\n     ++\treturn get_ssh_key_fingerprint(get_signing_key());\n     ++}\n     ++\n     ++/* Returns a textual but unique representation of the signing key */\n      +const char *get_signing_key_id(void)\n      +{\n     -+\tif (!strcmp(use_format->name, \"ssh\")) {\n     -+\t\treturn get_ssh_key_fingerprint(get_signing_key());\n     -+\t} else {\n     -+\t\t/* GPG/GPGSM only store a key id on this variable */\n     -+\t\treturn get_signing_key();\n     ++\tif (use_format->get_key_id) {\n     ++\t\treturn use_format->get_key_id();\n      +\t}\n     ++\n     ++\t/* GPG/GPGSM only store a key id on this variable */\n     ++\treturn get_signing_key();\n      +}\n      +\n       const char *get_signing_key(void)\n  5:  725764018ce !  6:  dc092c79796 ssh signing: parse ssh-keygen output and verify signatures\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    ssh signing: parse ssh-keygen output and verify signatures\n     +    ssh signing: verify signatures using ssh-keygen\n      \n     -    to verify a ssh signature we first call ssh-keygen -Y find-principal to\n     +    To verify a ssh signature we first call ssh-keygen -Y find-principal to\n          look up the signing principal by their public key from the\n          allowedSignersFile. If the key is found then we do a verify. Otherwise\n          we only validate the signature but can not verify the signers identity.\n     @@ Commit message\n          SIGNERS\") which contains valid public keys and a principal (usually\n          user@domain). Depending on the environment this file can be managed by\n          the individual developer or for example generated by the central\n     -    repository server from known ssh keys with push access. If the\n     -    repository only allows signed commits / pushes then the file can even be\n     -    stored inside it.\n     +    repository server from known ssh keys with push access. This file is usually\n     +    stored outside the repository, but if the repository only allows signed\n     +    commits/pushes, the user might choose to store it in the repository.\n      \n          To revoke a key put the public key without the principal prefix into\n          gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n     @@ Commit message\n          Using SSH CA Keys with these files is also possible. Add\n          \"cert-authority\" as key option between the principal and the key to mark\n          it as a CA and all keys signed by it as valid for this CA.\n     +    See \"CERTIFICATES\" in ssh-keygen(1).\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     + ## Documentation/config/gpg.txt ##\n     +@@ Documentation/config/gpg.txt: gpg.ssh.defaultKeyCommand:\n     + \tsignature is requested. On successful exit a valid ssh public key is\n     + \texpected in the\tfirst line of its output. To automatically use the first\n     + \tavailable key from your ssh-agent set this to \"ssh-add -L\".\n     ++\n     ++gpg.ssh.allowedSignersFile::\n     ++\tA file containing ssh public keys which you are willing to trust.\n     ++\tThe file consists of one or more lines of principals followed by an ssh\n     ++\tpublic key.\n     ++\te.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...\n     ++\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n     ++\tThe principal is only used to identify the key and is available when\n     ++\tverifying a signature.\n     +++\n     ++SSH has no concept of trust levels like gpg does. To be able to differentiate\n     ++between valid signatures and trusted signatures the trust level of a signature\n     ++verification is set to `fully` when the public key is present in the allowedSignersFile.\n     ++Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.\n     ++Otherwise valid but untrusted signatures will still verify but show no principal\n     ++name of the signer.\n     +++\n     ++This file can be set to a location outside of the repository and every developer\n     ++maintains their own trust store. A central repository server could generate this\n     ++file automatically from ssh keys with push access to verify the code against.\n     ++In a corporate setting this file is probably generated at a global location\n     ++from automation that already handles developer ssh keys.\n     +++\n     ++A repository that only allows signed commits can store the file\n     ++in the repository itself using a path relative to the top-level of the working tree.\n     ++This way only committers with an already valid key can add or change keys in the keyring.\n     +++\n     ++Using a SSH CA key with the cert-authority option\n     ++(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n     ++\n     ++gpg.ssh.revocationFile::\n     ++\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n     ++\tSee ssh-keygen(1) for details.\n     ++\tIf a public key is found in this file then it will always be treated\n     ++\tas having trust level \"never\" and signatures will show as invalid.\n     +\n       ## builtin/receive-pack.c ##\n      @@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const char *value, void *cb)\n       {\n       \tint status = parse_hide_refs_config(var, value, \"receive\");\n       \n     -+\tgit_gpg_config(var, value, NULL);\n     ++\tif (status)\n     ++\t\treturn status;\n      +\n     ++\tstatus = git_gpg_config(var, value, NULL);\n       \tif (status)\n       \t\treturn status;\n       \n     @@ gpg-interface.c\n       #include \"gpg-interface.h\"\n       #include \"sigchain.h\"\n       #include \"tempfile.h\"\n     + #include \"alias.h\"\n       \n       static char *configured_signing_key;\n     -+static const char *ssh_allowed_signers, *ssh_revocation_file;\n     +-static const char *ssh_default_key_command;\n     ++static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;\n       static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n       \n       struct gpg_format {\n     @@ gpg-interface.c: static struct gpg_format gpg_format[] = {\n       \t\t.sigs = ssh_sigs,\n      -\t\t.verify_signed_buffer = NULL, /* TODO */\n      +\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n     - \t\t.sign_buffer = sign_buffer_ssh\n     - \t},\n     - };\n     + \t\t.sign_buffer = sign_buffer_ssh,\n     + \t\t.get_default_key = get_default_ssh_signing_key,\n     + \t\t.get_key_id = get_ssh_key_id,\n      @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sigc,\n       \treturn ret;\n       }\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +static void parse_ssh_output(struct signature_check *sigc)\n      +{\n      +\tconst char *line, *principal, *search;\n     ++\tchar *key = NULL;\n      +\n      +\t/*\n     -+\t * ssh-keysign output should be:\n     ++\t * ssh-keygen output should be:\n      +\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n     -+\t * Good \"git\" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT\n     ++\t *\n      +\t * or for valid but unknown keys:\n      +\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n     ++\t *\n     ++\t * Note that \"PRINCIPAL\" can contain whitespace, \"RSA\" and\n     ++\t * \"SHA256\" part could be a different token that names of\n     ++\t * the algorithms used, and \"FINGERPRINT\" is a hexadecimal\n     ++\t * string.  By finding the last occurence of \" with \", we can\n     ++\t * reliably parse out the PRINCIPAL.\n      +\t */\n      +\tsigc->result = 'B';\n      +\tsigc->trust_level = TRUST_NEVER;\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\t\t\t\tline = search + 1;\n      +\t\t} while (search != NULL);\n      +\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n     -+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n     -+\t\tsigc->key = xstrdup(sigc->fingerprint);\n      +\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n      +\t\t/* Valid signature, but key unknown */\n      +\t\tsigc->result = 'G';\n      +\t\tsigc->trust_level = TRUST_UNDEFINED;\n     ++\t} else {\n     ++\t\treturn;\n     ++\t}\n     ++\n     ++\tkey = strstr(line, \"key\");\n     ++\tif (key) {\n      +\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n      +\t\tsigc->key = xstrdup(sigc->fingerprint);\n     ++\t} else {\n     ++\t\t/*\n     ++\t\t * Output did not match what we expected\n     ++\t\t * Treat the signature as bad\n     ++\t\t */\n     ++\t\tsigc->result = 'B';\n      +\t}\n      +}\n      +\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\t\tgoto out;\n      +\t}\n      +\tif (ret || !ssh_keygen_out.len) {\n     -+\t\t/* We did not find a matching principal in the allowedSigners - Check\n     -+\t\t * without validation */\n     ++\t\t/*\n     ++\t\t * We did not find a matching principal in the allowedSigners\n     ++\t\t * Check without validation\n     ++\t\t */\n      +\t\tchild_process_init(&ssh_keygen);\n      +\t\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n      +\t\t\t     \"-Y\", \"check-novalidate\",\n      +\t\t\t     \"-n\", \"git\",\n      +\t\t\t     \"-s\", buffer_file->filename.buf,\n      +\t\t\t     NULL);\n     -+\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n     ++\t\tpipe_command(&ssh_keygen, payload, payload_size,\n      +\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n     ++\n     ++\t\t/*\n     ++\t\t * Fail on unknown keys\n     ++\t\t * we still call check-novalidate to display the signature info\n     ++\t\t */\n     ++\t\tret = -1;\n      +\t} else {\n      +\t\t/* Check every principal we found (one per line) */\n      +\t\tfor (line = ssh_keygen_out.buf; *line;\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\t\t\tstrbuf_release(&ssh_keygen_out);\n      +\t\t\tstrbuf_release(&ssh_keygen_err);\n      +\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n     -+\t\t\t/* We found principals - Try with each until we find a\n     -+\t\t\t * match */\n     ++\t\t\t/*\n     ++\t\t\t * We found principals\n     ++\t\t\t * Try with each until we find a match\n     ++\t\t\t */\n      +\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\",\n      +\t\t\t\t     \"-n\", \"git\",\n      +\t\t\t\t     \"-f\", ssh_allowed_signers,\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\n      +\t\t\tFREE_AND_NULL(principal);\n      +\n     -+\t\t\tret &= starts_with(ssh_keygen_out.buf, \"Good\");\n     -+\t\t\tif (ret == 0)\n     ++\t\t\tif (!ret)\n     ++\t\t\t\tret = !starts_with(ssh_keygen_out.buf, \"Good\");\n     ++\n     ++\t\t\tif (!ret)\n      +\t\t\t\tbreak;\n      +\t\t}\n      +\t}\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n       \tsize_t slen, struct signature_check *sigc)\n       {\n      @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)\n     - \t\treturn 0;\n     + \t\treturn git_config_string(&ssh_default_key_command, var, value);\n       \t}\n       \n      +\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n      +\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n      +\t}\n      +\n     -+\tif (!strcmp(var, \"gpg.ssh.revocationFile\")) {\n     ++\tif (!strcmp(var, \"gpg.ssh.revocationfile\")) {\n      +\t\tif (!value)\n      +\t\t\treturn config_error_nonbool(var);\n      +\t\treturn git_config_string(&ssh_revocation_file, var, value);\n  7:  01da9a07934 !  7:  c17441566d9 ssh signing: duplicate t7510 tests for commits\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\n      +\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n      +\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n      +\n      +\techo 1 >file && git add file &&\n      +\ttest_tick && git commit -S -m initial &&\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n      +\tgit tag seventh-signed &&\n      +\n     -+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${SIGNING_KEY_UNTRUSTED}\" &&\n     ++\techo 8 >file && test_tick && git commit -a -m eighth -S\"${GPGSSH_KEY_UNTRUSTED}\" &&\n      +\tgit tag eighth-signed-alt &&\n      +\n      +\t# commit.gpgsign is still on but this must not be signed\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n      +\ttest_line_count = 1 oid &&\n      +\tgit tag eleventh-signed $(cat oid) &&\n     -+\techo 12 | git commit-tree --gpg-sign=\"${SIGNING_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n     ++\techo 12 | git commit-tree --gpg-sign=\"${GPGSSH_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n      +\ttest_line_count = 1 oid &&\n      +\tgit tag twelfth-signed-alt $(cat oid)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify and show signatures' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\ttest_config gpg.mintrustlevel UNDEFINED &&\n      +\t(\n      +\t\tfor commit in initial second merge fourth-signed \\\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\t\tdo\n      +\t\t\tgit verify-commit $commit &&\n      +\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $commit OK || exit 1\n      +\t\tdone\n      +\t) &&\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\t\tdo\n      +\t\t\ttest_must_fail git verify-commit $commit &&\n      +\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n     -+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $commit OK || exit 1\n      +\t\tdone\n      +\t) &&\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n      +\t\tdo\n      +\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n      +\t\t\techo $commit OK || exit 1\n      +\t\tdone\n      +\t)\n      +'\n      +\n     -+test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     -+\tgit verify-commit eighth-signed-alt 2>actual &&\n     -+\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     -+\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature' '\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n     ++\ttest_must_fail git verify-commit eighth-signed-alt 2>actual &&\n     ++\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\tgrep \"${KEY_NOT_TRUSTED}\" actual\n      +'\n      +\n      +test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\ttest_config gpg.minTrustLevel fully &&\n      +\tgit verify-commit sixth-signed\n      +'\n      +\n      +test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\ttest_config gpg.minTrustLevel marginal &&\n      +\tgit verify-commit sixth-signed\n      +'\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify signatures with --raw' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\t(\n      +\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n      +\t\tdo\n      +\t\t\tgit verify-commit --raw $commit 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $commit OK || exit 1\n      +\t\tdone\n      +\t) &&\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n      +\t\tdo\n      +\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n     -+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $commit OK || exit 1\n      +\t\tdone\n      +\t) &&\n      +\t(\n      +\t\tfor commit in eighth-signed-alt\n      +\t\tdo\n     -+\t\t\tgit verify-commit --raw $commit 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $commit OK || exit 1\n      +\t\tdone\n      +\t)\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show signed commit with signature' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit show -s initial >commit &&\n      +\tgit show -s --show-signature initial >show &&\n      +\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n      +\tgit cat-file commit initial >cat &&\n     -+\tgrep -v -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n     -+\tgrep -e \"${GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n     ++\tgrep -v -e \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n     ++\tgrep -e \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n      +\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n      +\ttest_cmp show.commit commit &&\n      +\ttest_cmp show.gpg verify.2 &&\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'detect fudged signature' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit cat-file commit seventh-signed >raw &&\n      +\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n      +\tgit hash-object -w -t commit forged1 >forged1.commit &&\n      +\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n      +\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n     -+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n     -+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n     -+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n     ++\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual1 &&\n     ++\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n     ++\t! grep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual1\n      +'\n      +\n      +test_expect_success GPGSSH 'detect fudged signature with NUL' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit cat-file commit seventh-signed >raw &&\n      +\tcat raw >forged2 &&\n      +\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n      +\tgit hash-object -w -t commit forged2 >forged2.commit &&\n      +\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n      +\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n     -+\tgrep \"${BAD_SIGNATURE}\" actual2 &&\n     -+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual2\n     ++\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual2 &&\n     ++\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual2\n      +'\n      +\n      +test_expect_success GPGSSH 'amending already signed commit' '\n      +\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit checkout fourth-signed^0 &&\n      +\tgit commit --amend -S --no-edit &&\n      +\tgit verify-commit HEAD &&\n      +\tgit show -s --show-signature HEAD >actual &&\n     -+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t! grep \"${BAD_SIGNATURE}\" actual\n     ++\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual\n      +'\n      +\n      +test_expect_success GPGSSH 'show good signature with custom format' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tG\n      +\tFINGERPRINT\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show bad signature with custom format' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect <<-\\EOF &&\n      +\tB\n      +\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'show untrusted signature with custom format' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tU\n      +\tFINGERPRINT\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\n      +\tEOF\n      +\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n      +\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n      +\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tundefined\n      +\tFINGERPRINT\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\n      +\tEOF\n      +\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n      +\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n      +\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect.tmpl <<-\\EOF &&\n      +\tfully\n      +\tFINGERPRINT\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\n      +\tEOF\n      +\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n      +\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +'\n      +\n      +test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\ttest_config log.showsignature true &&\n      +\tgit show initial >actual &&\n     -+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n     ++\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual\n      +'\n      +\n      +test_expect_success GPGSSH 'check config gpg.format values' '\n      +\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n      +\ttest_config gpg.format ssh &&\n      +\tgit commit -S --amend -m \"success\" &&\n      +\ttest_config gpg.format OpEnPgP &&\n  8:  d9707443f5c !  8:  0763517d62d ssh signing: add more tests for logs, tags & push certs\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    ssh signing: add more tests for logs, tags & push certs\n     +    ssh signing: tests for logs, tags & push certs\n      \n          Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n      \n     @@ t/t4202-log.sh: test_expect_success GPGSM 'setup signed branch x509' '\n       \n      +test_expect_success GPGSSH 'setup sshkey signed branch' '\n      +\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n      +\ttest_when_finished \"git reset --hard && git checkout main\" &&\n      +\tgit checkout -b signed-ssh main &&\n      +\techo foo >foo &&\n     @@ t/t4202-log.sh: test_expect_success GPGSM 'log OpenPGP fingerprint' '\n       '\n       \n      +test_expect_success GPGSSH 'log ssh key fingerprint' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     -+\tssh-keygen -lf  \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n     ++\tssh-keygen -lf  \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n      +\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t4202-log.sh: test_expect_success GPGSM 'log --graph --show-signature x509' '\n       '\n       \n      +test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n      +\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n      +'\n     @@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certific\n      +test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n      +\tprepare_dst &&\n      +\tmkdir -p dst/.git/hooks &&\n     -+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\tgit -C dst config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit -C dst config receive.certnonceseed sekrit &&\n      +\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n      +\t# discard the update list\n     @@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certific\n      +\tEOF\n      +\n      +\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n      +\tgit push --signed dst noop ff +noff &&\n      +\n      +\t(\n     @@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed use\n      +\ttest_config gpg.format ssh &&\n      +\tprepare_dst &&\n      +\tmkdir -p dst/.git/hooks &&\n     -+\tgit -C dst config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\tgit -C dst config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit -C dst config receive.certnonceseed sekrit &&\n      +\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n      +\t# discard the update list\n     @@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed use\n      +\t\tsane_unset GIT_COMMITTER_EMAIL &&\n      +\t\ttest_must_fail git push --signed dst noop ff +noff\n      +\t) &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     -+\tFINGERPRINT=$(ssh-keygen -lf \"${SIGNING_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n     ++\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n     ++\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n      +\tgit push --signed dst noop ff +noff &&\n      +\n      +\t(\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +test_expect_success GPGSSH 'create signed tags ssh' '\n      +\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n      +\ttest_config gpg.format ssh &&\n     -+\ttest_config user.signingkey \"${SIGNING_KEY_PRIMARY}\" &&\n     ++\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n      +\n      +\techo 1 >file && git add file &&\n      +\ttest_tick && git commit -m initial &&\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +\tgit tag -m seventh -s seventh-signed &&\n      +\n      +\techo 8 >file && test_tick && git commit -a -m eighth &&\n     -+\tgit tag -u\"${SIGNING_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n     ++\tgit tag -u\"${GPGSSH_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n      +'\n      +\n      +test_expect_success GPGSSH 'verify and show ssh signatures' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\t(\n      +\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n      +\t\tdo\n      +\t\t\tgit verify-tag $tag 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $tag OK || exit 1\n      +\t\tdone\n      +\t) &&\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n      +\t\tdo\n      +\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n     -+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $tag OK || exit 1\n      +\t\tdone\n      +\t) &&\n      +\t(\n      +\t\tfor tag in eighth-signed-alt\n      +\t\tdo\n     -+\t\t\tgit verify-tag $tag 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     -+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n     ++\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${GPGSSH_KEY_NOT_TRUSTED}\" actual &&\n      +\t\t\techo $tag OK || exit 1\n      +\t\tdone\n      +\t)\n      +'\n      +\n      +test_expect_success GPGSSH 'detect fudged ssh signature' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit cat-file tag seventh-signed >raw &&\n      +\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n      +\tgit hash-object -w -t tag forged1 >forged1.tag &&\n      +\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n     -+\tgrep \"${BAD_SIGNATURE}\" actual1 &&\n     -+\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n     -+\t! grep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual1\n     ++\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual1 &&\n     ++\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n     ++\t! grep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual1\n      +'\n      +\n      +test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\t(\n      +\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n      +\t\tdo\n      +\t\t\tgit verify-tag --raw $tag 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $tag OK || exit 1\n      +\t\tdone\n      +\t) &&\n     @@ t/t7031-verify-tag-signed-ssh.sh (new)\n      +\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n      +\t\tdo\n      +\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n     -+\t\t\t! grep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $tag OK || exit 1\n      +\t\tdone\n      +\t) &&\n      +\t(\n      +\t\tfor tag in eighth-signed-alt\n      +\t\tdo\n     -+\t\t\tgit verify-tag --raw $tag 2>actual &&\n     -+\t\t\tgrep \"${GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     -+\t\t\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n     ++\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n     ++\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\t\t\techo $tag OK || exit 1\n      +\t\tdone\n      +\t)\n      +'\n      +\n      +test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tgit verify-tag --raw sixth-signed 2>actual &&\n     -+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual &&\n     -+\t! grep \"${BAD_SIGNATURE}\" actual &&\n     ++\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n     ++\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n      +\techo sixth-signed OK\n      +'\n      +\n      +test_expect_success GPGSSH 'verify multiple tags ssh' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\ttags=\"seventh-signed sixth-signed\" &&\n      +\tfor i in $tags\n      +\tdo\n      +\t\tgit verify-tag -v --raw $i || return 1\n      +\tdone >expect.stdout 2>expect.stderr.1 &&\n     -+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n     ++\tgrep \"^${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n      +\tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n     -+\tgrep \"^${GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n     ++\tgrep \"^${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n      +\ttest_cmp expect.stdout actual.stdout &&\n      +\ttest_cmp expect.stderr actual.stderr\n      +'\n      +\n      +test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n     -+\ttest_config gpg.ssh.allowedSignersFile \"${SIGNING_ALLOWED_SIGNERS}\" &&\n     ++\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n      +\tcat >expect <<-\\EOF &&\n      +\ttagname : fourth-signed\n      +\tEOF\n  9:  275af516eba <  -:  ----------- ssh signing: add documentation\n  -:  ----------- >  9:  a5add98197a ssh signing: test that gpg fails for unkown keys\n\n-- \ngitgitgadget\n"},{"id":"431814","messageId":"fe98052a3ea76ae63fa7070f8490d16ccb8514e6.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 2/9] ssh signing: add test prereqs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:51Z","receivedAt":"2021-08-03T13:46:15Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nGenerate some ssh keys and a allowedSignersFile for testing\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/lib-gpg.sh | 28 ++++++++++++++++++++++++++++\n 1 file changed, 28 insertions(+)\n\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\nindex 9fc5241228e..f99ef3e859d 100644\n--- a/t/lib-gpg.sh\n+++ b/t/lib-gpg.sh\n@@ -87,6 +87,34 @@ test_lazy_prereq RFC1991 '\n \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n '\n \n+GPGSSH_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n+GPGSSH_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n+GPGSSH_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n+GPGSSH_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n+GPGSSH_KEY_PASSPHRASE=\"super_secret\"\n+GPGSSH_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n+\n+GPGSSH_GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n+GPGSSH_GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n+GPGSSH_KEY_NOT_TRUSTED=\"No principal matched\"\n+GPGSSH_BAD_SIGNATURE=\"Signature verification failed\"\n+\n+test_lazy_prereq GPGSSH '\n+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n+\ttest $? != 127 || exit 1\n+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n+\ttest $? = 0 || exit 1;\n+\tmkdir -p \"${GNUPGHOME}\" &&\n+\tchmod 0700 \"${GNUPGHOME}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GPGSSH_KEY_PRIMARY}\" >/dev/null &&\n+\techo \"\\\"principal with number 1\\\" $(cat \"${GPGSSH_KEY_PRIMARY}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GPGSSH_KEY_SECONDARY}\" >/dev/null &&\n+\techo \"\\\"principal with number 2\\\" $(cat \"${GPGSSH_KEY_SECONDARY}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -t ed25519 -N \"${GPGSSH_KEY_PASSPHRASE}\" -C \"git ed25519 encrypted key\" -f \"${GPGSSH_KEY_WITH_PASSPHRASE}\" >/dev/null &&\n+\techo \"\\\"principal with number 3\\\" $(cat \"${GPGSSH_KEY_WITH_PASSPHRASE}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GPGSSH_KEY_UNTRUSTED}\" >/dev/null\n+'\n+\n sanitize_pgp() {\n \tperl -ne '\n \t\t/^-----END PGP/ and $in_pgp = 0;\n-- \ngitgitgadget\n\n"},{"id":"431815","messageId":"80d2d55d22e4b424e798aff3993d9424bd2f9a02.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 3/9] ssh signing: add ssh key format and signing code","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:52Z","receivedAt":"2021-08-03T13:46:15Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nImplements the actual sign_buffer_ssh operation and move some shared\ncleanup code into a strbuf function\n\nSet gpg.format = ssh and user.signingkey to either a ssh public key\nstring (like from an authorized_keys file), or a ssh key file.\nIf the key file or the config value itself contains only a public key\nthen the private key needs to be available via ssh-agent.\n\ngpg.ssh.program can be set to an alternative location of ssh-keygen.\nA somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\nthis feature. Since only ssh-keygen is needed it can this way be\ninstalled seperately without upgrading your system openssh packages.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  |   4 +-\n Documentation/config/user.txt |   5 ++\n gpg-interface.c               | 138 ++++++++++++++++++++++++++++++++--\n 3 files changed, 137 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..88531b15f0f 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..2155128957c 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,8 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n+\tcorresponds to the private key used for signing. The private key\n+\tneeds to be available via ssh-agent. Alternatively it can be set to\n+\ta file containing a private key directly.\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex db54b054162..7ca682ac6d6 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = { NULL };\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n \n static struct gpg_format gpg_format[] = {\n \t{\n@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t},\n+\t{\n+\t\t.name = \"ssh\",\n+\t\t.program = \"ssh-keygen\",\n+\t\t.verify_args = ssh_verify_args,\n+\t\t.sigs = ssh_sigs,\n+\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.sign_buffer = sign_buffer_ssh\n+\t},\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+/*\n+ * Strip CR from the line endings, in case we are on Windows.\n+ * NEEDSWORK: make it trim only CRs before LFs and rename\n+ */\n+static void remove_cr_after(struct strbuf *buffer, size_t offset)\n+{\n+\tsize_t i, j;\n+\n+\tfor (i = j = offset; i < buffer->len; i++) {\n+\t\tif (buffer->buf[i] != '\\r') {\n+\t\t\tif (i != j)\n+\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n+\t\t\tj++;\n+\t\t}\n+\t}\n+\tstrbuf_setlen(buffer, j);\n+}\n+\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t  const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\n-\tsize_t i, j, bottom;\n+\tsize_t bottom;\n \tstruct strbuf gpg_status = STRBUF_INIT;\n \n \tstrvec_pushl(&gpg.args,\n@@ -494,13 +531,98 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\treturn error(_(\"gpg failed to sign the data\"));\n \n \t/* Strip CR from the line endings, in case we are on Windows. */\n-\tfor (i = j = bottom; i < signature->len; i++)\n-\t\tif (signature->buf[i] != '\\r') {\n-\t\t\tif (i != j)\n-\t\t\t\tsignature->buf[j] = signature->buf[i];\n-\t\t\tj++;\n-\t\t}\n-\tstrbuf_setlen(signature, j);\n+\tremove_cr_after(signature, bottom);\n \n \treturn 0;\n }\n+\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key)\n+{\n+\tstruct child_process signer = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tsize_t bottom, keylen;\n+\tstruct strbuf signer_stderr = STRBUF_INIT;\n+\tstruct tempfile *key_file = NULL, *buffer_file = NULL;\n+\tchar *ssh_signing_key_file = NULL;\n+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n+\n+\tif (!signing_key || signing_key[0] == '\\0')\n+\t\treturn error(\n+\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n+\n+\tif (starts_with(signing_key, \"ssh-\")) {\n+\t\t/* A literal ssh key */\n+\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\tif (!key_file)\n+\t\t\treturn error_errno(\n+\t\t\t\t_(\"could not create temporary file\"));\n+\t\tkeylen = strlen(signing_key);\n+\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n+\t\t    close_tempfile_gently(key_file) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n+\t\t\t\t    key_file->filename.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tssh_signing_key_file = strbuf_detach(&key_file->filename, NULL);\n+\t} else {\n+\t\t/* We assume a file */\n+\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n+\tif (!buffer_file) {\n+\t\terror_errno(_(\"could not create temporary file\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tstrvec_pushl(&signer.args, use_format->program,\n+\t\t     \"-Y\", \"sign\",\n+\t\t     \"-n\", \"git\",\n+\t\t     \"-f\", ssh_signing_key_file,\n+\t\t     buffer_file->filename.buf,\n+\t\t     NULL);\n+\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n+\tsigchain_pop(SIGPIPE);\n+\n+\tif (ret) {\n+\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n+\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n+\n+\t\terror(\"%s\", signer_stderr.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tbottom = signature->len;\n+\n+\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n+\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n+\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {\n+\t\terror_errno(\n+\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n+\t\t\tssh_signature_filename.buf);\n+\t}\n+\tunlink_or_warn(ssh_signature_filename.buf);\n+\n+\t/* Strip CR from the line endings, in case we are on Windows. */\n+\tremove_cr_after(signature, bottom);\n+\n+out:\n+\tif (key_file)\n+\t\tdelete_tempfile(&key_file);\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&signer_stderr);\n+\tstrbuf_release(&ssh_signature_filename);\n+\tFREE_AND_NULL(ssh_signing_key_file);\n+\treturn ret;\n+}\n-- \ngitgitgadget\n\n"},{"id":"431816","messageId":"83ece42e1dee09968444abe808e030c9210a5ba8.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 4/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:53Z","receivedAt":"2021-08-03T13:46:17Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nIf user.signingkey is not set and a ssh signature is requested we call\ngpg.ssh.defaultKeyCommand (typically \"ssh-add -L\") and use the first key we get\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  |  6 +++\n Documentation/config/user.txt |  4 +-\n gpg-interface.c               | 70 ++++++++++++++++++++++++++++++++++-\n 3 files changed, 77 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex 88531b15f0f..9b95dd280c3 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -33,3 +33,9 @@ gpg.minTrustLevel::\n * `marginal`\n * `fully`\n * `ultimate`\n+\n+gpg.ssh.defaultKeyCommand:\n+\tThis command that will be run when user.signingkey is not set and a ssh\n+\tsignature is requested. On successful exit a valid ssh public key is\n+\texpected in the\tfirst line of its output. To automatically use the first\n+\tavailable key from your ssh-agent set this to \"ssh-add -L\".\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 2155128957c..ad78dce9ecb 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -40,4 +40,6 @@ user.signingKey::\n \tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n \tcorresponds to the private key used for signing. The private key\n \tneeds to be available via ssh-agent. Alternatively it can be set to\n-\ta file containing a private key directly.\n+\ta file containing a private key directly. If not set git will call\n+\tgpg.ssh.defaultKeyCommand (e.g.: \"ssh-add -L\") and try to use the first\n+\tkey available.\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 7ca682ac6d6..3a0cca1b1d2 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -6,8 +6,10 @@\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n+#include \"alias.h\"\n \n static char *configured_signing_key;\n+static const char *ssh_default_key_command;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -21,6 +23,7 @@ struct gpg_format {\n \t\t\t\t    size_t signature_size);\n \tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n+\tconst char *(*get_default_key)(void);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -56,6 +59,8 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n \n+static const char *get_default_ssh_signing_key(void);\n+\n static struct gpg_format gpg_format[] = {\n \t{\n \t\t.name = \"openpgp\",\n@@ -64,6 +69,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.sigs = openpgp_sigs,\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n+\t\t.get_default_key = NULL,\n \t},\n \t{\n \t\t.name = \"x509\",\n@@ -72,6 +78,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.sigs = x509_sigs,\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n+\t\t.get_default_key = NULL,\n \t},\n \t{\n \t\t.name = \"ssh\",\n@@ -79,7 +86,8 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_args = ssh_verify_args,\n \t\t.sigs = ssh_sigs,\n \t\t.verify_signed_buffer = NULL, /* TODO */\n-\t\t.sign_buffer = sign_buffer_ssh\n+\t\t.sign_buffer = sign_buffer_ssh,\n+\t\t.get_default_key = get_default_ssh_signing_key,\n \t},\n };\n \n@@ -453,6 +461,12 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.defaultkeycommand\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_default_key_command, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \n@@ -470,11 +484,63 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+/* Returns the first public key from an ssh-agent to use for signing */\n+static const char *get_default_ssh_signing_key(void)\n+{\n+\tstruct child_process ssh_default_key = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf key_stdout = STRBUF_INIT, key_stderr = STRBUF_INIT;\n+\tstruct strbuf **keys;\n+\tchar *key_command = NULL;\n+\tconst char **argv;\n+\tint n;\n+\tchar *default_key = NULL;\n+\n+\tif (!ssh_default_key_command)\n+\t\tdie(_(\"either user.signingkey or gpg.ssh.defaultKeyCommand needs to be configured\"));\n+\n+\tkey_command = xstrdup(ssh_default_key_command);\n+\tn = split_cmdline(key_command, &argv);\n+\n+\tif (n < 0)\n+\t\tdie(\"malformed build-time gpg.ssh.defaultKeyCommand: %s\",\n+\t\t    split_cmdline_strerror(n));\n+\n+\tstrvec_pushv(&ssh_default_key.args, argv);\n+\tret = pipe_command(&ssh_default_key, NULL, 0, &key_stdout, 0,\n+\t\t\t   &key_stderr, 0);\n+\n+\tif (!ret) {\n+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n+\t\tif (keys[0] && starts_with(keys[0]->buf, \"ssh-\")) {\n+\t\t\tdefault_key = strbuf_detach(keys[0], NULL);\n+\t\t} else {\n+\t\t\twarning(_(\"gpg.ssh.defaultKeycommand succeeded but returned no keys: %s %s\"),\n+\t\t\t\tkey_stderr.buf, key_stdout.buf);\n+\t\t}\n+\n+\t\tstrbuf_list_free(keys);\n+\t} else {\n+\t\twarning(_(\"gpg.ssh.defaultKeyCommand failed: %s %s\"),\n+\t\t\tkey_stderr.buf, key_stdout.buf);\n+\t}\n+\n+\tfree(key_command);\n+\tfree(argv);\n+\tstrbuf_release(&key_stdout);\n+\n+\treturn default_key;\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (use_format->get_default_key) {\n+\t\treturn use_format->get_default_key();\n+\t}\n+\n+\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n-- \ngitgitgadget\n\n"},{"id":"431817","messageId":"76bc9eb407969a6d99a86072ef33894ed5948272.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 5/9] ssh signing: provide a textual signing_key_id","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:54Z","receivedAt":"2021-08-03T13:46:18Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nFor ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\nIn push certs and textual output we prefer the ssh fingerprint instead.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++\n gpg-interface.h |  6 ++++++\n send-pack.c     |  8 +++----\n 3 files changed, 66 insertions(+), 4 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 3a0cca1b1d2..0f1c6a02e53 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -24,6 +24,7 @@ struct gpg_format {\n \tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n \tconst char *(*get_default_key)(void);\n+\tconst char *(*get_key_id)(void);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -61,6 +62,8 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \n static const char *get_default_ssh_signing_key(void);\n \n+static const char *get_ssh_key_id(void);\n+\n static struct gpg_format gpg_format[] = {\n \t{\n \t\t.name = \"openpgp\",\n@@ -70,6 +73,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t\t.get_default_key = NULL,\n+\t\t.get_key_id = NULL,\n \t},\n \t{\n \t\t.name = \"x509\",\n@@ -79,6 +83,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t\t.get_default_key = NULL,\n+\t\t.get_key_id = NULL,\n \t},\n \t{\n \t\t.name = \"ssh\",\n@@ -88,6 +93,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = NULL, /* TODO */\n \t\t.sign_buffer = sign_buffer_ssh,\n \t\t.get_default_key = get_default_ssh_signing_key,\n+\t\t.get_key_id = get_ssh_key_id,\n \t},\n };\n \n@@ -484,6 +490,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+static char *get_ssh_key_fingerprint(const char *signing_key)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n+\tstruct strbuf **fingerprint;\n+\n+\t/*\n+\t * With SSH Signing this can contain a filename or a public key\n+\t * For textual representation we usually want a fingerprint\n+\t */\n+\tif (starts_with(signing_key, \"ssh-\")) {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n+\t\tret = pipe_command(&ssh_keygen, signing_key,\n+\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t} else {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\",\n+\t\t\t     configured_signing_key, NULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t}\n+\n+\tif (!!ret)\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n+\tif (!fingerprint[1])\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\treturn strbuf_detach(fingerprint[1], NULL);\n+}\n+\n /* Returns the first public key from an ssh-agent to use for signing */\n static const char *get_default_ssh_signing_key(void)\n {\n@@ -532,6 +573,21 @@ static const char *get_default_ssh_signing_key(void)\n \treturn default_key;\n }\n \n+static const char *get_ssh_key_id(void) {\n+\treturn get_ssh_key_fingerprint(get_signing_key());\n+}\n+\n+/* Returns a textual but unique representation of the signing key */\n+const char *get_signing_key_id(void)\n+{\n+\tif (use_format->get_key_id) {\n+\t\treturn use_format->get_key_id();\n+\t}\n+\n+\t/* GPG/GPGSM only store a key id on this variable */\n+\treturn get_signing_key();\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex feac4decf8b..beefacbb1e9 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+\n+/*\n+ * Returns a textual unique representation of the signing key in use\n+ * Either a GPG KeyID or a SSH Key Fingerprint\n+ */\n+const char *get_signing_key_id(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\ndiff --git a/send-pack.c b/send-pack.c\nindex 5a79e0e7110..50cca7e439b 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n {\n \tconst struct ref *ref;\n \tstruct string_list_item *item;\n-\tchar *signing_key = xstrdup(get_signing_key());\n+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n \tconst char *cp, *np;\n \tstruct strbuf cert = STRBUF_INIT;\n \tint update_seen = 0;\n \n \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n \tdatestamp(&cert);\n \tstrbuf_addch(&cert, '\\n');\n \tif (args->url && *args->url) {\n@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, get_signing_key()))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tpacket_buf_write(req_buf, \"push-cert-end\\n\");\n \n free_return:\n-\tfree(signing_key);\n+\tfree(signing_key_id);\n \tstrbuf_release(&cert);\n \treturn update_seen;\n }\n-- \ngitgitgadget\n\n"},{"id":"431818","messageId":"c17441566d9518c27180d1e552bfee54c21bb7bf.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 7/9] ssh signing: duplicate t7510 tests for commits","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:56Z","receivedAt":"2021-08-03T13:46:21Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++\n 1 file changed, 398 insertions(+)\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\ndiff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh\nnew file mode 100755\nindex 00000000000..3e093168eef\n--- /dev/null\n+++ b/t/t7528-signed-commit-ssh.sh\n@@ -0,0 +1,398 @@\n+#!/bin/sh\n+\n+test_description='ssh signed commit tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed commits' '\n+\ttest_oid_cache <<-\\EOF &&\n+\theader sha1:gpgsig\n+\theader sha256:gpgsig-sha256\n+\tEOF\n+\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -S -m initial &&\n+\tgit tag initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -S -m second &&\n+\tgit tag second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n+\tgit tag fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag fourth-signed &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n+\tgit tag fifth-signed &&\n+\n+\tgit config commit.gpgsign false &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag sixth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n+\tgit tag seventh-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n+\tgit tag seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${GPGSSH_KEY_UNTRUSTED}\" &&\n+\tgit tag eighth-signed-alt &&\n+\n+\t# commit.gpgsign is still on but this must not be signed\n+\techo 9 | git commit-tree HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag ninth-unsigned $(cat oid) &&\n+\t# explicit -S of course must sign.\n+\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag tenth-signed $(cat oid) &&\n+\n+\t# --gpg-sign[=<key-id>] must sign.\n+\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag eleventh-signed $(cat oid) &&\n+\techo 12 | git commit-tree --gpg-sign=\"${GPGSSH_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag twelfth-signed-alt $(cat oid)\n+'\n+\n+test_expect_success GPGSSH 'verify and show signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed \\\n+\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n+\t\t\televenth-signed\n+\t\tdo\n+\t\t\tgit verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n+\t\t\tseventh-unsigned ninth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n+\t\tdo\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git verify-commit eighth-signed-alt 2>actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\tgrep \"${KEY_NOT_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n+\tgit cat-file commit fourth-signed >output &&\n+\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n+'\n+\n+test_expect_success GPGSSH 'show signed commit with signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit show -s initial >commit &&\n+\tgit show -s --show-signature initial >show &&\n+\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n+\tgit cat-file commit initial >cat &&\n+\tgrep -v -e \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n+\tgrep -e \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n+\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n+\ttest_cmp show.commit commit &&\n+\ttest_cmp show.gpg verify.2 &&\n+\ttest_cmp cat.commit verify.1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t commit forged1 >forged1.commit &&\n+\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n+\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature with NUL' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tcat raw >forged2 &&\n+\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n+\tgit hash-object -w -t commit forged2 >forged2.commit &&\n+\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n+\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual2 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual2\n+'\n+\n+test_expect_success GPGSSH 'amending already signed commit' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit checkout fourth-signed^0 &&\n+\tgit commit --amend -S --no-edit &&\n+\tgit verify-commit HEAD &&\n+\tgit show -s --show-signature HEAD >actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual\n+'\n+\n+test_expect_success GPGSSH 'show good signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tG\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show bad signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\tB\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tU\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tundefined\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tfully\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show lack of signature with custom format' '\n+\tcat >expect <<-\\EOF &&\n+\tN\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config log.showsignature true &&\n+\tgit show initial >actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'check config gpg.format values' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\ttest_config gpg.format ssh &&\n+\tgit commit -S --amend -m \"success\" &&\n+\ttest_config gpg.format OpEnPgP &&\n+\ttest_must_fail git commit -S --amend -m \"fail\"\n+'\n+\n+test_expect_failure GPGSSH 'detect fudged commit with double signature (TODO)' '\n+\tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n+\tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n+\t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n+\tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n+\tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n+\tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n+\t\tdouble-combined.asc > double-gpgsig &&\n+\tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n+\tgit hash-object -w -t commit double-commit >double-commit.commit &&\n+\ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n+\tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n+\tgrep \"BAD signature from\" double-actual &&\n+\tgrep \"Good signature from\" double-actual\n+'\n+\n+test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '\n+\tcat >expect <<-\\EOF &&\n+\tE\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '\n+\tgit init multiply-signed &&\n+\tcd multiply-signed &&\n+\ttest_commit first &&\n+\techo 1 >second &&\n+\tgit add second &&\n+\ttree=$(git write-tree) &&\n+\tparent=$(git rev-parse HEAD^{commit}) &&\n+\tgit commit --gpg-sign -m second &&\n+\tgit cat-file commit HEAD &&\n+\t# Avoid trailing whitespace.\n+\tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n+\tQtree $tree\n+\tQparent $parent\n+\tQauthor A U Thor <author@example.com> 1112912653 -0700\n+\tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n+\tQgpgsig -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n+\tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n+\tQ =tQ0N\n+\tQ -----END PGP SIGNATURE-----\n+\tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n+\tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n+\tQ =pIwP\n+\tQ -----END PGP SIGNATURE-----\n+\tQ\n+\tQsecond\n+\tEOF\n+\thead=$(git hash-object -t commit -w commit) &&\n+\tgit reset --hard $head &&\n+\tgit verify-commit $head 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"431819","messageId":"dc092c7979618d252f7a5a0a5bbe497d8011fd5b.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 6/9] ssh signing: verify signatures using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:55Z","receivedAt":"2021-08-03T13:46:21Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nTo verify a ssh signature we first call ssh-keygen -Y find-principal to\nlook up the signing principal by their public key from the\nallowedSignersFile. If the key is found then we do a verify. Otherwise\nwe only validate the signature but can not verify the signers identity.\n\nVerification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\nSIGNERS\") which contains valid public keys and a principal (usually\nuser@domain). Depending on the environment this file can be managed by\nthe individual developer or for example generated by the central\nrepository server from known ssh keys with push access. This file is usually\nstored outside the repository, but if the repository only allows signed\ncommits/pushes, the user might choose to store it in the repository.\n\nTo revoke a key put the public key without the principal prefix into\ngpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n\"KEY REVOCATION LISTS\"). The same considerations about who to trust for\nverification as with the allowedSignersFile apply.\n\nUsing SSH CA Keys with these files is also possible. Add\n\"cert-authority\" as key option between the principal and the key to mark\nit as a CA and all keys signed by it as valid for this CA.\nSee \"CERTIFICATES\" in ssh-keygen(1).\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt |  35 ++++++\n builtin/receive-pack.c       |   4 +\n gpg-interface.c              | 209 ++++++++++++++++++++++++++++++++++-\n 3 files changed, 246 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex 9b95dd280c3..51a756b2f15 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -39,3 +39,38 @@ gpg.ssh.defaultKeyCommand:\n \tsignature is requested. On successful exit a valid ssh public key is\n \texpected in the\tfirst line of its output. To automatically use the first\n \tavailable key from your ssh-agent set this to \"ssh-add -L\".\n+\n+gpg.ssh.allowedSignersFile::\n+\tA file containing ssh public keys which you are willing to trust.\n+\tThe file consists of one or more lines of principals followed by an ssh\n+\tpublic key.\n+\te.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...\n+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n+\tThe principal is only used to identify the key and is available when\n+\tverifying a signature.\n++\n+SSH has no concept of trust levels like gpg does. To be able to differentiate\n+between valid signatures and trusted signatures the trust level of a signature\n+verification is set to `fully` when the public key is present in the allowedSignersFile.\n+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.\n+Otherwise valid but untrusted signatures will still verify but show no principal\n+name of the signer.\n++\n+This file can be set to a location outside of the repository and every developer\n+maintains their own trust store. A central repository server could generate this\n+file automatically from ssh keys with push access to verify the code against.\n+In a corporate setting this file is probably generated at a global location\n+from automation that already handles developer ssh keys.\n++\n+A repository that only allows signed commits can store the file\n+in the repository itself using a path relative to the top-level of the working tree.\n+This way only committers with an already valid key can add or change keys in the keyring.\n++\n+Using a SSH CA key with the cert-authority option\n+(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n+\n+gpg.ssh.revocationFile::\n+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n+\tSee ssh-keygen(1) for details.\n+\tIf a public key is found in this file then it will always be treated\n+\tas having trust level \"never\" and signatures will show as invalid.\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 2d1f97e1ca7..05dc8e160f8 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -131,6 +131,10 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n {\n \tint status = parse_hide_refs_config(var, value, \"receive\");\n \n+\tif (status)\n+\t\treturn status;\n+\n+\tstatus = git_gpg_config(var, value, NULL);\n \tif (status)\n \t\treturn status;\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0f1c6a02e53..9c1ef11a563 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -3,13 +3,14 @@\n #include \"config.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n+#include \"dir.h\"\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n #include \"alias.h\"\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command;\n+static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -55,6 +56,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n@@ -90,7 +95,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.program = \"ssh-keygen\",\n \t\t.verify_args = ssh_verify_args,\n \t\t.sigs = ssh_sigs,\n-\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n \t\t.sign_buffer = sign_buffer_ssh,\n \t\t.get_default_key = get_default_ssh_signing_key,\n \t\t.get_key_id = get_ssh_key_id,\n@@ -357,6 +362,194 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \treturn ret;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tconst char *line, *principal, *search;\n+\tchar *key = NULL;\n+\n+\t/*\n+\t * ssh-keygen output should be:\n+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\t *\n+\t * or for valid but unknown keys:\n+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n+\t *\n+\t * Note that \"PRINCIPAL\" can contain whitespace, \"RSA\" and\n+\t * \"SHA256\" part could be a different token that names of\n+\t * the algorithms used, and \"FINGERPRINT\" is a hexadecimal\n+\t * string.  By finding the last occurence of \" with \", we can\n+\t * reliably parse out the PRINCIPAL.\n+\t */\n+\tsigc->result = 'B';\n+\tsigc->trust_level = TRUST_NEVER;\n+\n+\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n+\n+\tif (skip_prefix(line, \"Good \\\"git\\\" signature for \", &line)) {\n+\t\t/* Valid signature and known principal */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_FULLY;\n+\n+\t\t/* Search for the last \"with\" to get the full principal */\n+\t\tprincipal = line;\n+\t\tdo {\n+\t\t\tsearch = strstr(line, \" with \");\n+\t\t\tif (search)\n+\t\t\t\tline = search + 1;\n+\t\t} while (search != NULL);\n+\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n+\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n+\t\t/* Valid signature, but key unknown */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_UNDEFINED;\n+\t} else {\n+\t\treturn;\n+\t}\n+\n+\tkey = strstr(line, \"key\");\n+\tif (key) {\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t} else {\n+\t\t/*\n+\t\t * Output did not match what we expected\n+\t\t * Treat the signature as bad\n+\t\t */\n+\t\tsigc->result = 'B';\n+\t}\n+}\n+\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tstruct tempfile *buffer_file;\n+\tint ret = -1;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n+\n+\tif (!ssh_allowed_signers) {\n+\t\terror(_(\"gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification\"));\n+\t\treturn -1;\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n+\tif (!buffer_file)\n+\t\treturn error_errno(_(\"could not create temporary file\"));\n+\tif (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tdelete_tempfile(&buffer_file);\n+\t\treturn -1;\n+\t}\n+\n+\t/* Find the principal from the signers */\n+\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t     \"-Y\", \"find-principals\",\n+\t\t     \"-f\", ssh_allowed_signers,\n+\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t     NULL);\n+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n+\t\t\t   &ssh_keygen_err, 0);\n+\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n+\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n+\t\tgoto out;\n+\t}\n+\tif (ret || !ssh_keygen_out.len) {\n+\t\t/*\n+\t\t * We did not find a matching principal in the allowedSigners\n+\t\t * Check without validation\n+\t\t */\n+\t\tchild_process_init(&ssh_keygen);\n+\t\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t\t     \"-Y\", \"check-novalidate\",\n+\t\t\t     \"-n\", \"git\",\n+\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t     NULL);\n+\t\tpipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\n+\t\t/*\n+\t\t * Fail on unknown keys\n+\t\t * we still call check-novalidate to display the signature info\n+\t\t */\n+\t\tret = -1;\n+\t} else {\n+\t\t/* Check every principal we found (one per line) */\n+\t\tfor (line = ssh_keygen_out.buf; *line;\n+\t\t     line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n+\n+\t\t\ttrust_size = strcspn(line, \"\\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n+\n+\t\t\tchild_process_init(&ssh_keygen);\n+\t\t\tstrbuf_release(&ssh_keygen_out);\n+\t\t\tstrbuf_release(&ssh_keygen_err);\n+\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n+\t\t\t/*\n+\t\t\t * We found principals\n+\t\t\t * Try with each until we find a match\n+\t\t\t */\n+\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\",\n+\t\t\t\t     \"-n\", \"git\",\n+\t\t\t\t     \"-f\", ssh_allowed_signers,\n+\t\t\t\t     \"-I\", principal,\n+\t\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t\t     NULL);\n+\n+\t\t\tif (ssh_revocation_file) {\n+\t\t\t\tif (file_exists(ssh_revocation_file)) {\n+\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\",\n+\t\t\t\t\t\t     ssh_revocation_file, NULL);\n+\t\t\t\t} else {\n+\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"),\n+\t\t\t\t\t\tssh_revocation_file);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n+\n+\t\t\tFREE_AND_NULL(principal);\n+\n+\t\t\tif (!ret)\n+\t\t\t\tret = !starts_with(ssh_keygen_out.buf, \"Good\");\n+\n+\t\t\tif (!ret)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n+\tsigc->gpg_status = xstrdup(sigc->output);\n+\n+\tparse_ssh_output(sigc);\n+\n+out:\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&ssh_keygen_out);\n+\tstrbuf_release(&ssh_keygen_err);\n+\n+\treturn ret;\n+}\n+\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n@@ -473,6 +666,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn git_config_string(&ssh_default_key_command, var, value);\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationfile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \n-- \ngitgitgadget\n\n"},{"id":"431820","messageId":"0763517d62d25c4ddb907570628ae64db5e14e41.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 8/9] ssh signing: tests for logs, tags & push certs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:57Z","receivedAt":"2021-08-03T13:46:51Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t4202-log.sh                   |  23 +++++\n t/t5534-push-signed.sh           | 101 +++++++++++++++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++\n 3 files changed, 285 insertions(+)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 9dfead936b7..6a650dacd6e 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' '\n \tgit commit -S -m signed_commit\n '\n \n+test_expect_success GPGSSH 'setup sshkey signed branch' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n+\tgit checkout -b signed-ssh main &&\n+\techo foo >foo &&\n+\tgit add foo &&\n+\tgit commit -S -m signed_commit\n+'\n+\n test_expect_success GPGSM 'log x509 fingerprint' '\n \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPGSSH 'log ssh key fingerprint' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -lf  \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature' '\n \tgit log --graph --show-signature -n1 signed >actual &&\n \tgrep \"^| gpg: Signature made\" actual &&\n@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' '\n \tgrep \"^| gpgsm: Good signature\" actual\n '\n \n+test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature for merged tag' '\n \ttest_when_finished \"git reset --hard && git checkout main\" &&\n \tgit checkout -b plain main &&\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex bba768f5ded..24d374adbae 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n \t# First, invoke receive-pack with dummy input to obtain its preamble.\n \tprepare_dst &&\n@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n+\ttest_config gpg.format ssh &&\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config user.email hasnokey@nowhere.com &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"\" &&\n+\t(\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git push --signed dst noop ff +noff\n+\t) &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'failed atomic push does not execute GPG' '\n \tprepare_dst &&\n \tgit -C dst config receive.certnonceseed sekrit &&\ndiff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh\nnew file mode 100755\nindex 00000000000..06c9dd6c933\n--- /dev/null\n+++ b/t/t7031-verify-tag-signed-ssh.sh\n@@ -0,0 +1,161 @@\n+#!/bin/sh\n+\n+test_description='signed tag tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed tags ssh' '\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -m initial &&\n+\tgit tag -s -m initial initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -m second &&\n+\tgit tag -s -m second second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag -s -m merge merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag -s -m fourth fourth-signed &&\n+\n+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n+\tgit tag fifth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag -a -m sixth sixth-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n+\tgit tag -m seventh -s seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth &&\n+\tgit tag -u\"${GPGSSH_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify and show ssh signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${GPGSSH_KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'detect fudged ssh signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file tag seventh-signed >raw &&\n+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n+\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit verify-tag --raw sixth-signed 2>actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\techo sixth-signed OK\n+'\n+\n+test_expect_success GPGSSH 'verify multiple tags ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttags=\"seventh-signed sixth-signed\" &&\n+\tfor i in $tags\n+\tdo\n+\t\tgit verify-tag -v --raw $i || return 1\n+\tdone >expect.stdout 2>expect.stderr.1 &&\n+\tgrep \"^${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n+\tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+\tgrep \"^${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n+\ttest_cmp expect.stdout actual.stdout &&\n+\ttest_cmp expect.stderr actual.stderr\n+'\n+\n+test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\ttagname : fourth-signed\n+\tEOF\n+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently - ssh' '\n+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n+\ttest_must_be_empty actual-forged\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"431821","messageId":"a5add98197a9a92a84d5ba386a9e801878e6461a.1627998358.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v7 9/9] ssh signing: test that gpg fails for unkown keys","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-08-03T13:45:58Z","receivedAt":"2021-08-03T13:46:54Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nTest that verify-commit/tag will fail when a gpg key is completely\nunknown. To do this we have to generate a key, use it for a signature\nand delete it from our keyring aferwards completely.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7510-signed-commit.sh | 29 ++++++++++++++++++++++++++++-\n 1 file changed, 28 insertions(+), 1 deletion(-)\n\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 8df5a74f1db..d65a0171f29 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -71,7 +71,25 @@ test_expect_success GPG 'create signed commits' '\n \tgit tag eleventh-signed $(cat oid) &&\n \techo 12 | git commit-tree --gpg-sign=B7227189 HEAD^{tree} >oid &&\n \ttest_line_count = 1 oid &&\n-\tgit tag twelfth-signed-alt $(cat oid)\n+\tgit tag twelfth-signed-alt $(cat oid) &&\n+\n+\tcat >keydetails <<-\\EOF &&\n+\tKey-Type: RSA\n+\tKey-Length: 2048\n+\tSubkey-Type: RSA\n+\tSubkey-Length: 2048\n+\tName-Real: Unknown User\n+\tName-Email: unknown@git.com\n+\tExpire-Date: 0\n+\t%no-ask-passphrase\n+\t%no-protection\n+\tEOF\n+\tgpg --batch --gen-key keydetails &&\n+\techo 13 >file && git commit -a -S\"unknown@git.com\" -m thirteenth &&\n+\tgit tag thirteenth-signed &&\n+\tDELETE_FINGERPRINT=$(gpg -K --with-colons --fingerprint --batch unknown@git.com | grep \"^fpr\" | head -n 1 | awk -F \":\" \"{print \\$10;}\") &&\n+\tgpg --batch --yes --delete-secret-keys $DELETE_FINGERPRINT &&\n+\tgpg --batch --yes --delete-keys unknown@git.com\n '\n \n test_expect_success GPG 'verify and show signatures' '\n@@ -110,6 +128,13 @@ test_expect_success GPG 'verify and show signatures' '\n \t)\n '\n \n+test_expect_success GPG 'verify-commit exits failure on unknown signature' '\n+\ttest_must_fail git verify-commit thirteenth-signed 2>actual &&\n+\t! grep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\tgrep -q -F -e \"No public key\" -e \"public key not found\" actual\n+'\n+\n test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n \tgit verify-commit eighth-signed-alt 2>actual &&\n \tgrep \"Good signature from\" actual &&\n@@ -338,6 +363,8 @@ test_expect_success GPG 'show double signature with custom format' '\n '\n \n \n+# NEEDSWORK: This test relies on the test_tick commit/author dates from the first\n+# 'create signed commits' test even though it creates its own\n test_expect_success GPG 'verify-commit verifies multiply signed commits' '\n \tgit init multiply-signed &&\n \tcd multiply-signed &&\n-- \ngitgitgadget\n"},{"id":"431903","messageId":"xmqqzgtyrszl.fsf@gitster.g","threadId":"56054","inReplyTo":"dc092c7979618d252f7a5a0a5bbe497d8011fd5b.1627998358.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 6/9] ssh signing: verify signatures using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-03T23:47:10Z","receivedAt":"2021-08-03T23:47:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 2d1f97e1ca7..05dc8e160f8 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -131,6 +131,10 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n>  {\n>  \tint status = parse_hide_refs_config(var, value, \"receive\");\n>  \n> +\tif (status)\n> +\t\treturn status;\n> +\n> +\tstatus = git_gpg_config(var, value, NULL);\n>  \tif (status)\n>  \t\treturn status;\n\nHmph, it feels a bit odd for a misconfigured \"transfer.hiderefs\" to\nprevent GPG related configuration from getting read, but is this\nbecause a failure from receive_pack_config() will immediately kill\nthe process without doing any harm to the system?  If so, the code\nis good as written.\n\n"},{"id":"431945","messageId":"fb7c504f-90f7-9a79-1903-e3942f43e808@gigacodes.de","threadId":"56054","inReplyTo":"xmqqzgtyrszl.fsf@gitster.g","subject":"Re: [PATCH v7 6/9] ssh signing: verify signatures using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-08-04T09:01:53Z","receivedAt":"2021-08-04T09:01:58Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"\n\nOn 04.08.21 01:47, Junio C Hamano wrote:\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n>> index 2d1f97e1ca7..05dc8e160f8 100644\n>> --- a/builtin/receive-pack.c\n>> +++ b/builtin/receive-pack.c\n>> @@ -131,6 +131,10 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n>>   {\n>>   \tint status = parse_hide_refs_config(var, value, \"receive\");\n>>   \n>> +\tif (status)\n>> +\t\treturn status;\n>> +\n>> +\tstatus = git_gpg_config(var, value, NULL);\n>>   \tif (status)\n>>   \t\treturn status;\n> \n> Hmph, it feels a bit odd for a misconfigured \"transfer.hiderefs\" to\n> prevent GPG related configuration from getting read, but is this\n> because a failure from receive_pack_config() will immediately kill\n> the process without doing any harm to the system?  If so, the code\n> is good as written.\n> \n\nI think i misunderstood the comment from Jonathan about this. He wrote:\n\n\"Check the return value of git_gpg_config() to see if that config was\nprocessed by that function - if yes, we can return early.\"\n\nLooking at git_gpg_config i don't think i can actually determine by its \nreturn code if a value was successfully processed (it will also return 0 \nwhen nothing happened).\n\nThe return in case parse_hide_refs fails was already in place before my \nchange and returning on git_gpg_config failure is done in most of the \nother commands calling it. builtin/send-pack.c is the exception but i \nhave no idea why.\n\nGenerally i think a broken config should die() early as it does in this \ncase with the return.\n"},{"id":"431967","messageId":"xmqqo8adp13f.fsf@gitster.g","threadId":"56054","inReplyTo":"fb7c504f-90f7-9a79-1903-e3942f43e808@gigacodes.de","subject":"Re: [PATCH v7 6/9] ssh signing: verify signatures using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-04T17:32:36Z","receivedAt":"2021-08-04T17:32:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n> Generally i think a broken config should die() early as it does in\n> this case with the return.\n\nYes, I was just making sure if somebody took a look at the callchain\nto make sure it dies, as I didn't ;-)\n\nThanks.\n"},{"id":"434030","messageId":"xmqqczpv99u4.fsf@gitster.g","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-29T22:15:15Z","receivedAt":"2021-08-29T22:15:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> openssh 8.7 will add valid-after, valid-before options to the allowed keys\n> keyring. This allows us to pass the commit timestamp to the verification\n> call and make key rollover possible and still be able to verify older\n> commits. Set valid-after to the current date when adding your key to the\n> keyring and set valid-before to make it fail if used after a certain date.\n> Software like gitolite/github or corporate automation can do this\n> automatically when ssh push keys are addded / removed I will add this\n> feature in a follow up patch afterwards.\n\nHas this follow-on work happened already?\n\nThe previous rounds saw enough reviews and responses, but this round\ndidn't.  Usually no response means no interest from the community,\nbut let's see if somebody other than the author actually tried the\nfeature, and and want to tell us about their experience, either\npositive or negative?\n\nAs the basic step of the topic, possibly to be built upon laster, I\nam tempted to say that this v7 may want to be cooked in 'next' for\nwider exposure.\n\nI'll typofix the topmost commit before doing so, though.\n\nThanks.\n\n\n\n1:  4ff5911494 ! 1:  b88bcd013b ssh signing: test that gpg fails for unkown keys\n    @@ Metadata\n     Author: Fabian Stelzer <fs@gigacodes.de>\n     \n      ## Commit message ##\n    -    ssh signing: test that gpg fails for unkown keys\n    +    ssh signing: test that gpg fails for unknown keys\n     \n         Test that verify-commit/tag will fail when a gpg key is completely\n         unknown. To do this we have to generate a key, use it for a signature\n"},{"id":"434034","messageId":"YSweouO5B4gD3XLB@tilde.club","threadId":"56054","inReplyTo":"xmqqczpv99u4.fsf@gitster.g","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Gwyneth Morgan","fromEmail":"gwymor@tilde.club","sentAt":"2021-08-29T23:56:18Z","receivedAt":"2021-08-29T23:56:44Z","isPatch":true,"sender":{"key":"gwymor@tilde.club","avatar":"https://avatars.githubusercontent.com/u/87623694?v=4"},"body":"On 2021-08-29 15:15:15-0700, Junio C Hamano wrote:\n> The previous rounds saw enough reviews and responses, but this round\n> didn't.  Usually no response means no interest from the community,\n> but let's see if somebody other than the author actually tried the\n> feature, and and want to tell us about their experience, either\n> positive or negative?\n\nI've been using this feature (including this round) on and off and I've\nbeen happy with it. I ran into a small bug in an earlier version which\nhas since been fixed, but other than that I haven't had any issues. The\nsetup and use is all pretty easy.\n\nAdmittedly, I haven't been daily-driving this feature, as I didn't want\nto put SSH-signed commits in repositories in case the format changes in\nthe future.\n"},{"id":"434058","messageId":"9075cdd1-e34d-5dcb-f2b8-69ae4abf587b@gigacodes.de","threadId":"56054","inReplyTo":"xmqqczpv99u4.fsf@gitster.g","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-08-30T10:35:07Z","receivedAt":"2021-08-30T10:35:17Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 30.08.21 00:15, Junio C Hamano wrote:\n\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> openssh 8.7 will add valid-after, valid-before options to the allowed keys\n>> keyring. This allows us to pass the commit timestamp to the verification\n>> call and make key rollover possible and still be able to verify older\n>> commits. Set valid-after to the current date when adding your key to the\n>> keyring and set valid-before to make it fail if used after a certain date.\n>> Software like gitolite/github or corporate automation can do this\n>> automatically when ssh push keys are addded / removed I will add this\n>> feature in a follow up patch afterwards.\n> Has this follow-on work happened already?\nI have this prepared but not ready for submission. I wanted to wait\nuntil openssh 8.7 is released (which happened recently) to make sure\ntheir api for this newly added feature does not change.\nI will be on vacation for the next 2 weeks but can submit it afterwards.\nI have a few additional features in mind but wanted to wait for the\nbasic functionality to settle before piling stuff on top.\nI'd like to add a \"Trust on First Use\" mode that will add keys to your\nallowedSIgners File when encountered the first time (this could very\nsimilar to how .ssh/known/hosts works).\nThe idea came from here: https://lwn.net/Articles/803619/\nAlso signing support for git format-patch/am would be nice (ssh\nsignatures are much smaller then gpg and shouldnt be too bad in emails.\nNot as minimal as minisign but with easier/more established key handling)\n>\n> The previous rounds saw enough reviews and responses, but this round\n> didn't.  Usually no response means no interest from the community,\n> but let's see if somebody other than the author actually tried the\n> feature, and and want to tell us about their experience, either\n> positive or negative?\nI will roll this out to our corporate env after my vacation but can\nunderstand that people are hesitant to push commits with it since older\ngit versions will BUG() on verification of the new signatures.\nBut at least github handles it well (\"GitHub supports GPG and S/MIME\nsignatures. We don’t know what type of signature this is.\"). I have not\ntested with other Forges yet.\n>\n> As the basic step of the topic, possibly to be built upon laster, I\n> am tempted to say that this v7 may want to be cooked in 'next' for\n> wider exposure.\n>\n> I'll typofix the topmost commit before doing so, though.\nThanks\n"},{"id":"434901","messageId":"xmqq4kawcmqg.fsf@gitster.g","threadId":"56054","inReplyTo":"9075cdd1-e34d-5dcb-f2b8-69ae4abf587b@gigacodes.de","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-09-07T17:35:35Z","receivedAt":"2021-09-07T17:35:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n> I have this prepared but not ready for submission. I wanted to wait\n> until openssh 8.7 is released (which happened recently) to make sure\n> their api for this newly added feature does not change.\n> I will be on vacation for the next 2 weeks but can submit it afterwards.\n> I have a few additional features in mind but wanted to wait for the\n> basic functionality to settle before piling stuff on top.\n\nReasonable.\n\nIn the meantime, people seem to be finding issues with OpenSSH 8.7's\nkeygen, so before doing any *new* things, we'd like to see an update\nto make the stuff already posted and reviewed to work with the newer\nOpenSSH.  Hoping that the fix for the incompatibility with 8.7 is\nsmall enough, I am planning to keep the version we already have in\nour tree (in 'next' but not in 'master'), so that an incremental\npatch will be able to highlight what the differences are when the\nbug is fixed.\n\nAfter the dust settles, of course, trust on first use may be one of\nthe first sensible thing to add, and there may be other enhancements,\nbut let's see a solid base to build upon.\n\nAnd please continue enjoying your vacation ;-) Looking forwared to\nhearing from you when you come back.\n\n\n[Reference]\n\n* https://lore.kernel.org/git/CAPUEspgnRFNRoFuEvP1hpY3iKukk3OnF4zk85wkdkmiVuPuRTw@mail.gmail.com/\n"},{"id":"435371","messageId":"4500892e-9efe-550c-73fa-37a3a69bc737@gigacodes.de","threadId":"56054","inReplyTo":"xmqq4kawcmqg.fsf@gitster.g","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-09-10T08:03:59Z","receivedAt":"2021-09-10T08:07:13Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 07.09.21 19:35, Junio C Hamano wrote:\n\n> Fabian Stelzer <fs@gigacodes.de> writes:\n>\n>> I have this prepared but not ready for submission. I wanted to wait\n>> until openssh 8.7 is released (which happened recently) to make sure\n>> their api for this newly added feature does not change.\n>> I will be on vacation for the next 2 weeks but can submit it afterwards.\n>> I have a few additional features in mind but wanted to wait for the\n>> basic functionality to settle before piling stuff on top.\n> Reasonable.\n>\n> In the meantime, people seem to be finding issues with OpenSSH 8.7's\n> keygen, so before doing any *new* things, we'd like to see an update\n> to make the stuff already posted and reviewed to work with the newer\n> OpenSSH.  Hoping that the fix for the incompatibility with 8.7 is\n> small enough, I am planning to keep the version we already have in\n> our tree (in 'next' but not in 'master'), so that an incremental\n> patch will be able to highlight what the differences are when the\n> bug is fixed.\n\nIt it not so much an incompatibility but a hard bug in ssh-keygen of my\nown making :/\nThere is nothing we can do on the git side to fix this since the\nfind-principal call will always segfault no matter what.\nI added an optional parameter some time ago for printing the public key\non verify to make \"trust on first use\" easier when we get to it.\nUnfortunately this bug made it into 8.7 but is already fixed in master.\nThanks to Carlo for spotting it and sending a patch.\nI guess i owe openssh writing a test for it since the command seems to\nnot have any at all.\n\nI'm not sure how git wants to handle this since i don't know when a\nfixed openssh release will be available and we certainly shouldn't\ninclude the signing feature in a release until they do.\nI can't really find a way of detecting the broken version since there's\nno version or anything else i could find in the ssh-keygen tool.\n\nI will continue writing some tests for the verify-time/key validity\nfeature. The tests will need some version/feature detection from\nssh-keygen as well so maybe i will still stumble on something that\nallows us to detect and warn on this.\n\n\n\n"},{"id":"435459","messageId":"xmqqsfycs21q.fsf@gitster.g","threadId":"56054","inReplyTo":"4500892e-9efe-550c-73fa-37a3a69bc737@gigacodes.de","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-09-10T18:44:49Z","receivedAt":"2021-09-10T18:45:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n> It it not so much an incompatibility but a hard bug in ssh-keygen of my\n> own making :/\n> There is nothing we can do on the git side to fix this since the\n> find-principal call will always segfault no matter what.\n\nSo... we cannot do anythying utnil a corrected OpenSSH is made\navailable, but once we can link with a corrected one, do we need to\ndo anything further on the patches in your topic?\n\nI am guessing that the ideal endgame would be that we can merge what\nwe have down to 'master' and ship it in a release with a note that\nsays \"OpenSSH 8.7 is broken---do not use the ssh signing feature if\nyou cannot update to OpenSSH X.Y (or stay at 8.6)\", and that is why\nI haven't kicked the topic out of 'next' and kept it there.\n\n> I will continue writing some tests for the verify-time/key validity\n> feature. The tests will need some version/feature detection from\n> ssh-keygen as well so maybe i will still stumble on something that\n> allows us to detect and warn on this.\n\nThanks.\n"},{"id":"435464","messageId":"532d97e7-8c91-df6a-6d90-70668256f513@gigacodes.de","threadId":"56054","inReplyTo":"xmqqsfycs21q.fsf@gitster.g","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-09-10T19:49:12Z","receivedAt":"2021-09-10T19:49:24Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 10.09.21 20:44, Junio C Hamano wrote:\n\n> Fabian Stelzer <fs@gigacodes.de> writes:\n>\n>> It it not so much an incompatibility but a hard bug in ssh-keygen of my\n>> own making :/\n>> There is nothing we can do on the git side to fix this since the\n>> find-principal call will always segfault no matter what.\n> So... we cannot do anythying utnil a corrected OpenSSH is made\n> available, but once we can link with a corrected one, do we need to\n> do anything further on the patches in your topic?\n\n\nOpenSSH will probably release a new version in October.\nI will send a new diff of my patch in a bit after the CI runs are\nthrough fixing a bug with some buffers that could sometimes lead to\nmemory corruption (i war releasing a buffer while still iterating over\nits contents), a small test fix and a minor improvement using\ngit_config_pathname instead of string.\nBesides that i think its good.\n\nFor the key lifetime changes that require openssh 8.7 i will send a new\npatchset afterwards.\n\n>\n> I am guessing that the ideal endgame would be that we can merge what\n> we have down to 'master' and ship it in a release with a note that\n> says \"OpenSSH 8.7 is broken---do not use the ssh signing feature if\n> you cannot update to OpenSSH X.Y (or stay at 8.6)\", and that is why\n> I haven't kicked the topic out of 'next' and kept it there.\n\nSounds good to me.\nThanks\n\n"},{"id":"435466","messageId":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v7.git.git.1627998358.gitgitgadget@gmail.com","subject":"[PATCH v8 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:33Z","receivedAt":"2021-09-10T20:07:47Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"openssh 8.7 will add valid-after, valid-before options to the allowed keys\nkeyring. This allows us to pass the commit timestamp to the verification\ncall and make key rollover possible and still be able to verify older\ncommits. Set valid-after to the current date when adding your key to the\nkeyring and set valid-before to make it fail if used after a certain date.\nSoftware like gitolite/github or corporate automation can do this\nautomatically when ssh push keys are addded / removed I will add this\nfeature in a follow up patch afterwards since the released 8.7 version has a\nbroken ssh-keygen implementation which will break ssh signing completely.\n\nv7:\n\n * change unknown signing key behavior to fail verify-commit/tag just like\n   gpg does\n * add test for unknown signing keys for ssh & gpg\n * made default signing key retrieval configurable\n   (gpg.ssh.defaultKeyCommand). We could default this to \"ssh-add -L\" but\n   would risk some users signing with a wrong key\n * die() instead of error in case of incompatible signatures to match\n   current BUG() behaviour more\n * various review fixes (early return for config parse, missing free,\n   comments)\n * got rid of strcmp(\"ssh\") branches and used format configurable callbacks\n   everywhere\n * moved documentation changes into the commits adding the specific\n   functionality\n\nv8:\n\n * fixes a bug around find-principals buffer i was releasing while still\n   iterating over it. Uses separate strbufs now.\n * rename a wrong variable in the tests\n * use git_config_pathname instead of string where applicable\n\nFabian Stelzer (9):\n  ssh signing: preliminary refactoring and clean-up\n  ssh signing: add test prereqs\n  ssh signing: add ssh key format and signing code\n  ssh signing: retrieve a default key from ssh-agent\n  ssh signing: provide a textual signing_key_id\n  ssh signing: verify signatures using ssh-keygen\n  ssh signing: duplicate t7510 tests for commits\n  ssh signing: tests for logs, tags & push certs\n  ssh signing: test that gpg fails for unknown keys\n\n Documentation/config/gpg.txt     |  45 ++-\n Documentation/config/user.txt    |   7 +\n builtin/receive-pack.c           |   4 +\n fmt-merge-msg.c                  |   6 +-\n gpg-interface.c                  | 577 ++++++++++++++++++++++++++++---\n gpg-interface.h                  |   8 +-\n log-tree.c                       |   8 +-\n pretty.c                         |   4 +-\n send-pack.c                      |   8 +-\n t/lib-gpg.sh                     |  28 ++\n t/t4202-log.sh                   |  23 ++\n t/t5534-push-signed.sh           | 101 ++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++\n t/t7510-signed-commit.sh         |  29 +-\n t/t7528-signed-commit-ssh.sh     | 398 +++++++++++++++++++++\n 15 files changed, 1341 insertions(+), 66 deletions(-)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\n\nbase-commit: 8463beaeb69fe0b7f651065813def4aa6827cd5d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v8\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v8\nPull-Request: https://github.com/git/git/pull/1041\n\nRange-diff vs v7:\n\n  1:  91fd0159e1f =  1:  b0bee197a05 ssh signing: preliminary refactoring and clean-up\n  2:  fe98052a3ea =  2:  d08327ecb25 ssh signing: add test prereqs\n  3:  80d2d55d22e =  3:  c1e9bba8da0 ssh signing: add ssh key format and signing code\n  4:  83ece42e1de =  4:  8c430fc7a1b ssh signing: retrieve a default key from ssh-agent\n  5:  76bc9eb4079 =  5:  0864ed04670 ssh signing: provide a textual signing_key_id\n  6:  dc092c79796 !  6:  cfd66180249 ssh signing: verify signatures using ssh-keygen\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\tconst char *line;\n      +\tsize_t trust_size;\n      +\tchar *principal;\n     ++\tstruct strbuf ssh_principals_out = STRBUF_INIT;\n     ++\tstruct strbuf ssh_principals_err = STRBUF_INIT;\n      +\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n      +\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n      +\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\t\t     \"-f\", ssh_allowed_signers,\n      +\t\t     \"-s\", buffer_file->filename.buf,\n      +\t\t     NULL);\n     -+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,\n     -+\t\t\t   &ssh_keygen_err, 0);\n     -+\tif (ret && strstr(ssh_keygen_err.buf, \"usage:\")) {\n     ++\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0,\n     ++\t\t\t   &ssh_principals_err, 0);\n     ++\tif (ret && strstr(ssh_principals_err.buf, \"usage:\")) {\n      +\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n      +\t\tgoto out;\n      +\t}\n     -+\tif (ret || !ssh_keygen_out.len) {\n     ++\tif (ret || !ssh_principals_out.len) {\n      +\t\t/*\n      +\t\t * We did not find a matching principal in the allowedSigners\n      +\t\t * Check without validation\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\t\tret = -1;\n      +\t} else {\n      +\t\t/* Check every principal we found (one per line) */\n     -+\t\tfor (line = ssh_keygen_out.buf; *line;\n     ++\t\tfor (line = ssh_principals_out.buf; *line;\n      +\t\t     line = strchrnul(line + 1, '\\n')) {\n      +\t\t\twhile (*line == '\\n')\n      +\t\t\t\tline++;\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +\tsigc->payload = xmemdupz(payload, payload_size);\n      +\tstrbuf_stripspace(&ssh_keygen_out, 0);\n      +\tstrbuf_stripspace(&ssh_keygen_err, 0);\n     ++\t/* Add stderr outputs to show the user actual ssh-keygen errors */\n     ++\tstrbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);\n      +\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n      +\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n      +\tsigc->gpg_status = xstrdup(sigc->output);\n     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig\n      +out:\n      +\tif (buffer_file)\n      +\t\tdelete_tempfile(&buffer_file);\n     ++\tstrbuf_release(&ssh_principals_out);\n     ++\tstrbuf_release(&ssh_principals_err);\n      +\tstrbuf_release(&ssh_keygen_out);\n      +\tstrbuf_release(&ssh_keygen_err);\n      +\n     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb\n      +\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n      +\t\tif (!value)\n      +\t\t\treturn config_error_nonbool(var);\n     -+\t\treturn git_config_string(&ssh_allowed_signers, var, value);\n     ++\t\treturn git_config_pathname(&ssh_allowed_signers, var, value);\n      +\t}\n      +\n      +\tif (!strcmp(var, \"gpg.ssh.revocationfile\")) {\n      +\t\tif (!value)\n      +\t\t\treturn config_error_nonbool(var);\n     -+\t\treturn git_config_string(&ssh_revocation_file, var, value);\n     ++\t\treturn git_config_pathname(&ssh_revocation_file, var, value);\n      +\t}\n      +\n       \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n  7:  c17441566d9 !  7:  c8e21dc97f1 ssh signing: duplicate t7510 tests for commits\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n      +\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n      +\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n     -+\t\t\tgrep \"${KEY_NOT_TRUSTED}\" actual &&\n     ++\t\t\tgrep \"${GPGSSH_KEY_NOT_TRUSTED}\" actual &&\n      +\t\t\techo $commit OK || exit 1\n      +\t\tdone\n      +\t)\n     @@ t/t7528-signed-commit-ssh.sh (new)\n      +\ttest_must_fail git verify-commit eighth-signed-alt 2>actual &&\n      +\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n      +\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n     -+\tgrep \"${KEY_NOT_TRUSTED}\" actual\n     ++\tgrep \"${GPGSSH_KEY_NOT_TRUSTED}\" actual\n      +'\n      +\n      +test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n  8:  0763517d62d =  8:  b66e3e0284c ssh signing: tests for logs, tags & push certs\n  9:  a5add98197a !  9:  07afb94ed83 ssh signing: test that gpg fails for unkown keys\n     @@ Metadata\n      Author: Fabian Stelzer <fs@gigacodes.de>\n      \n       ## Commit message ##\n     -    ssh signing: test that gpg fails for unkown keys\n     +    ssh signing: test that gpg fails for unknown keys\n      \n          Test that verify-commit/tag will fail when a gpg key is completely\n          unknown. To do this we have to generate a key, use it for a signature\n\n-- \ngitgitgadget\n"},{"id":"435467","messageId":"b0bee197a051f066936bfa1875809eb3990270a0.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 1/9] ssh signing: preliminary refactoring and clean-up","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:34Z","receivedAt":"2021-09-10T20:07:48Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nOpenssh v8.2p1 added some new options to ssh-keygen for signature\ncreation and verification. These allow us to use ssh keys for git\nsignatures easily.\n\nIn our corporate environment we use PIV x509 Certs on Yubikeys for email\nsigning/encryption and ssh keys which I think is quite common\n(at least for the email part). This way we can establish the correct\ntrust for the SSH Keys without setting up a separate GPG Infrastructure\n(which is still quite painful for users) or implementing x509 signing\nsupport for git (which lacks good forwarding mechanisms).\nUsing ssh agent forwarding makes this feature easily usable in todays\ndevelopment environments where code is often checked out in remote VMs / containers.\nIn such a setup the keyring & revocationKeyring can be centrally\ngenerated from the x509 CA information and distributed to the users.\n\nTo be able to implement new signing formats this commit:\n - makes the sigc structure more generic by renaming \"gpg_output\" to\n   \"output\"\n - introduces function pointers in the gpg_format structure to call\n   format specific signing and verification functions\n - moves format detection from verify_signed_buffer into the check_signature\n   api function and calls the format specific verify\n - renames and wraps sign_buffer to handle format specific signing logic\n   as well\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n fmt-merge-msg.c |   6 +--\n gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------\n gpg-interface.h |   2 +-\n log-tree.c      |   8 ++--\n pretty.c        |   4 +-\n 5 files changed, 74 insertions(+), 50 deletions(-)\n\ndiff --git a/fmt-merge-msg.c b/fmt-merge-msg.c\nindex b969dc6ebb6..2901c5e4f8f 100644\n--- a/fmt-merge-msg.c\n+++ b/fmt-merge-msg.c\n@@ -528,11 +528,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out)\n \t\t\tbuf = payload.buf;\n \t\t\tlen = payload.len;\n \t\t\tif (check_signature(payload.buf, payload.len, sig.buf,\n-\t\t\t\t\t sig.len, &sigc) &&\n-\t\t\t\t!sigc.gpg_output)\n+\t\t\t\t\t    sig.len, &sigc) &&\n+\t\t\t    !sigc.output)\n \t\t\t\tstrbuf_addstr(&sig, \"gpg verification failed.\\n\");\n \t\t\telse\n-\t\t\t\tstrbuf_addstr(&sig, sigc.gpg_output);\n+\t\t\t\tstrbuf_addstr(&sig, sigc.output);\n \t\t}\n \t\tsignature_check_clear(&sigc);\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 127aecfc2b0..db54b054162 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -15,6 +15,12 @@ struct gpg_format {\n \tconst char *program;\n \tconst char **verify_args;\n \tconst char **sigs;\n+\tint (*verify_signed_buffer)(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+\tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n+\n static struct gpg_format gpg_format[] = {\n-\t{ .name = \"openpgp\", .program = \"gpg\",\n-\t  .verify_args = openpgp_verify_args,\n-\t  .sigs = openpgp_sigs\n+\t{\n+\t\t.name = \"openpgp\",\n+\t\t.program = \"gpg\",\n+\t\t.verify_args = openpgp_verify_args,\n+\t\t.sigs = openpgp_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n-\t{ .name = \"x509\", .program = \"gpgsm\",\n-\t  .verify_args = x509_verify_args,\n-\t  .sigs = x509_sigs\n+\t{\n+\t\t.name = \"x509\",\n+\t\t.program = \"gpgsm\",\n+\t\t.verify_args = x509_verify_args,\n+\t\t.sigs = x509_sigs,\n+\t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n+\t\t.sign_buffer = sign_buffer_gpg,\n \t},\n };\n \n@@ -72,7 +93,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\n-\tFREE_AND_NULL(sigc->gpg_output);\n+\tFREE_AND_NULL(sigc->output);\n \tFREE_AND_NULL(sigc->gpg_status);\n \tFREE_AND_NULL(sigc->signer);\n \tFREE_AND_NULL(sigc->key);\n@@ -257,16 +278,16 @@ error:\n \tFREE_AND_NULL(sigc->key);\n }\n \n-static int verify_signed_buffer(const char *payload, size_t payload_size,\n-\t\t\t\tconst char *signature, size_t signature_size,\n-\t\t\t\tstruct strbuf *gpg_output,\n-\t\t\t\tstruct strbuf *gpg_status)\n+static int verify_gpg_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n-\tstruct gpg_format *fmt;\n \tstruct tempfile *temp;\n \tint ret;\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf gpg_stdout = STRBUF_INIT;\n+\tstruct strbuf gpg_stderr = STRBUF_INIT;\n \n \ttemp = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n \tif (!temp)\n@@ -279,10 +300,6 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\treturn -1;\n \t}\n \n-\tfmt = get_format_by_sig(signature);\n-\tif (!fmt)\n-\t\tBUG(\"bad signature '%s'\", signature);\n-\n \tstrvec_push(&gpg.args, fmt->program);\n \tstrvec_pushv(&gpg.args, fmt->verify_args);\n \tstrvec_pushl(&gpg.args,\n@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n \t\t     \"--verify\", temp->filename.buf, \"-\",\n \t\t     NULL);\n \n-\tif (!gpg_status)\n-\t\tgpg_status = &buf;\n-\n \tsigchain_push(SIGPIPE, SIG_IGN);\n-\tret = pipe_command(&gpg, payload, payload_size,\n-\t\t\t   gpg_status, 0, gpg_output, 0);\n+\tret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,\n+\t\t\t   &gpg_stderr, 0);\n \tsigchain_pop(SIGPIPE);\n \n \tdelete_tempfile(&temp);\n \n-\tret |= !strstr(gpg_status->buf, \"\\n[GNUPG:] GOODSIG \");\n-\tstrbuf_release(&buf); /* no matter it was used or not */\n+\tret |= !strstr(gpg_stdout.buf, \"\\n[GNUPG:] GOODSIG \");\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tsigc->output = strbuf_detach(&gpg_stderr, NULL);\n+\tsigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);\n+\n+\tparse_gpg_output(sigc);\n+\n+\tstrbuf_release(&gpg_stdout);\n+\tstrbuf_release(&gpg_stderr);\n \n \treturn ret;\n }\n@@ -309,35 +330,32 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n-\tstruct strbuf gpg_output = STRBUF_INIT;\n-\tstruct strbuf gpg_status = STRBUF_INIT;\n+\tstruct gpg_format *fmt;\n \tint status;\n \n \tsigc->result = 'N';\n \tsigc->trust_level = -1;\n \n-\tstatus = verify_signed_buffer(payload, plen, signature, slen,\n-\t\t\t\t      &gpg_output, &gpg_status);\n-\tif (status && !gpg_output.len)\n-\t\tgoto out;\n-\tsigc->payload = xmemdupz(payload, plen);\n-\tsigc->gpg_output = strbuf_detach(&gpg_output, NULL);\n-\tsigc->gpg_status = strbuf_detach(&gpg_status, NULL);\n-\tparse_gpg_output(sigc);\n+\tfmt = get_format_by_sig(signature);\n+\tif (!fmt)\n+\t\tdie(_(\"bad/incompatible signature '%s'\"), signature);\n+\n+\tstatus = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,\n+\t\t\t\t\t   slen);\n+\n+\tif (status && !sigc->output)\n+\t\treturn !!status;\n+\n \tstatus |= sigc->result != 'G';\n \tstatus |= sigc->trust_level < configured_min_trust_level;\n \n- out:\n-\tstrbuf_release(&gpg_status);\n-\tstrbuf_release(&gpg_output);\n-\n \treturn !!status;\n }\n \n void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n {\n-\tconst char *output = flags & GPG_VERIFY_RAW ?\n-\t\tsigc->gpg_status : sigc->gpg_output;\n+\tconst char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :\n+\t\t\t\t\t\t\t    sigc->output;\n \n \tif (flags & GPG_VERIFY_VERBOSE && sigc->payload)\n \t\tfputs(sigc->payload, stdout);\n@@ -441,6 +459,12 @@ const char *get_signing_key(void)\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n+{\n+\treturn use_format->sign_buffer(buffer, signature, signing_key);\n+}\n+\n+static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t  const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 80567e48948..feac4decf8b 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -17,7 +17,7 @@ enum signature_trust_level {\n \n struct signature_check {\n \tchar *payload;\n-\tchar *gpg_output;\n+\tchar *output;\n \tchar *gpg_status;\n \n \t/*\ndiff --git a/log-tree.c b/log-tree.c\nindex 6dc4412268b..644893fd8cf 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -515,10 +515,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit)\n \n \tstatus = check_signature(payload.buf, payload.len, signature.buf,\n \t\t\t\t signature.len, &sigc);\n-\tif (status && !sigc.gpg_output)\n+\tif (status && !sigc.output)\n \t\tshow_sig_lines(opt, status, \"No signature\\n\");\n \telse\n-\t\tshow_sig_lines(opt, status, sigc.gpg_output);\n+\t\tshow_sig_lines(opt, status, sigc.output);\n \tsignature_check_clear(&sigc);\n \n  out:\n@@ -585,8 +585,8 @@ static int show_one_mergetag(struct commit *commit,\n \t\t/* could have a good signature */\n \t\tstatus = check_signature(payload.buf, payload.len,\n \t\t\t\t\t signature.buf, signature.len, &sigc);\n-\t\tif (sigc.gpg_output)\n-\t\t\tstrbuf_addstr(&verify_message, sigc.gpg_output);\n+\t\tif (sigc.output)\n+\t\t\tstrbuf_addstr(&verify_message, sigc.output);\n \t\telse\n \t\t\tstrbuf_addstr(&verify_message, \"No signature\\n\");\n \t\tsignature_check_clear(&sigc);\ndiff --git a/pretty.c b/pretty.c\nindex 9631529c10a..be477bd51f2 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\tcheck_commit_signature(c->commit, &(c->signature_check));\n \t\tswitch (placeholder[1]) {\n \t\tcase 'G':\n-\t\t\tif (c->signature_check.gpg_output)\n-\t\t\t\tstrbuf_addstr(sb, c->signature_check.gpg_output);\n+\t\t\tif (c->signature_check.output)\n+\t\t\t\tstrbuf_addstr(sb, c->signature_check.output);\n \t\t\tbreak;\n \t\tcase '?':\n \t\t\tswitch (c->signature_check.result) {\n-- \ngitgitgadget\n\n"},{"id":"435468","messageId":"d08327ecb259296eb899a1e2c9fdda04fa8a0521.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 2/9] ssh signing: add test prereqs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:35Z","receivedAt":"2021-09-10T20:07:48Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nGenerate some ssh keys and a allowedSignersFile for testing\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/lib-gpg.sh | 28 ++++++++++++++++++++++++++++\n 1 file changed, 28 insertions(+)\n\ndiff --git a/t/lib-gpg.sh b/t/lib-gpg.sh\nindex 9fc5241228e..f99ef3e859d 100644\n--- a/t/lib-gpg.sh\n+++ b/t/lib-gpg.sh\n@@ -87,6 +87,34 @@ test_lazy_prereq RFC1991 '\n \techo | gpg --homedir \"${GNUPGHOME}\" -b --rfc1991 >/dev/null\n '\n \n+GPGSSH_KEY_PRIMARY=\"${GNUPGHOME}/ed25519_ssh_signing_key\"\n+GPGSSH_KEY_SECONDARY=\"${GNUPGHOME}/rsa_2048_ssh_signing_key\"\n+GPGSSH_KEY_UNTRUSTED=\"${GNUPGHOME}/untrusted_ssh_signing_key\"\n+GPGSSH_KEY_WITH_PASSPHRASE=\"${GNUPGHOME}/protected_ssh_signing_key\"\n+GPGSSH_KEY_PASSPHRASE=\"super_secret\"\n+GPGSSH_ALLOWED_SIGNERS=\"${GNUPGHOME}/ssh.all_valid.allowedSignersFile\"\n+\n+GPGSSH_GOOD_SIGNATURE_TRUSTED='Good \"git\" signature for'\n+GPGSSH_GOOD_SIGNATURE_UNTRUSTED='Good \"git\" signature with'\n+GPGSSH_KEY_NOT_TRUSTED=\"No principal matched\"\n+GPGSSH_BAD_SIGNATURE=\"Signature verification failed\"\n+\n+test_lazy_prereq GPGSSH '\n+\tssh_version=$(ssh-keygen -Y find-principals -n \"git\" 2>&1)\n+\ttest $? != 127 || exit 1\n+\techo $ssh_version | grep -q \"find-principals:missing signature file\"\n+\ttest $? = 0 || exit 1;\n+\tmkdir -p \"${GNUPGHOME}\" &&\n+\tchmod 0700 \"${GNUPGHOME}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -C \"git ed25519 key\" -f \"${GPGSSH_KEY_PRIMARY}\" >/dev/null &&\n+\techo \"\\\"principal with number 1\\\" $(cat \"${GPGSSH_KEY_PRIMARY}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -t rsa -b 2048 -N \"\" -C \"git rsa2048 key\" -f \"${GPGSSH_KEY_SECONDARY}\" >/dev/null &&\n+\techo \"\\\"principal with number 2\\\" $(cat \"${GPGSSH_KEY_SECONDARY}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -t ed25519 -N \"${GPGSSH_KEY_PASSPHRASE}\" -C \"git ed25519 encrypted key\" -f \"${GPGSSH_KEY_WITH_PASSPHRASE}\" >/dev/null &&\n+\techo \"\\\"principal with number 3\\\" $(cat \"${GPGSSH_KEY_WITH_PASSPHRASE}.pub\")\" >> \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -t ed25519 -N \"\" -f \"${GPGSSH_KEY_UNTRUSTED}\" >/dev/null\n+'\n+\n sanitize_pgp() {\n \tperl -ne '\n \t\t/^-----END PGP/ and $in_pgp = 0;\n-- \ngitgitgadget\n\n"},{"id":"435469","messageId":"c1e9bba8da09d464739e3a1e192d1d9c4cd29e24.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 3/9] ssh signing: add ssh key format and signing code","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:36Z","receivedAt":"2021-09-10T20:07:54Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nImplements the actual sign_buffer_ssh operation and move some shared\ncleanup code into a strbuf function\n\nSet gpg.format = ssh and user.signingkey to either a ssh public key\nstring (like from an authorized_keys file), or a ssh key file.\nIf the key file or the config value itself contains only a public key\nthen the private key needs to be available via ssh-agent.\n\ngpg.ssh.program can be set to an alternative location of ssh-keygen.\nA somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for\nthis feature. Since only ssh-keygen is needed it can this way be\ninstalled seperately without upgrading your system openssh packages.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  |   4 +-\n Documentation/config/user.txt |   5 ++\n gpg-interface.c               | 138 ++++++++++++++++++++++++++++++++--\n 3 files changed, 137 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex d94025cb368..88531b15f0f 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -11,13 +11,13 @@ gpg.program::\n \n gpg.format::\n \tSpecifies which key format to use when signing with `--gpg-sign`.\n-\tDefault is \"openpgp\" and another possible value is \"x509\".\n+\tDefault is \"openpgp\". Other possible values are \"x509\", \"ssh\".\n \n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\n \tbe used as a legacy synonym for `gpg.openpgp.program`. The default\n-\tvalue for `gpg.x509.program` is \"gpgsm\".\n+\tvalue for `gpg.x509.program` is \"gpgsm\" and `gpg.ssh.program` is \"ssh-keygen\".\n \n gpg.minTrustLevel::\n \tSpecifies a minimum trust level for signature verification.  If\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 59aec7c3aed..2155128957c 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -36,3 +36,8 @@ user.signingKey::\n \tcommit, you can override the default selection with this variable.\n \tThis option is passed unchanged to gpg's --local-user parameter,\n \tso you may specify a key using any method that gpg supports.\n+\tIf gpg.format is set to \"ssh\" this can contain the literal ssh public\n+\tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n+\tcorresponds to the private key used for signing. The private key\n+\tneeds to be available via ssh-agent. Alternatively it can be set to\n+\ta file containing a private key directly.\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex db54b054162..7ca682ac6d6 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {\n \tNULL\n };\n \n+static const char *ssh_verify_args[] = { NULL };\n+static const char *ssh_sigs[] = {\n+\t\"-----BEGIN SSH SIGNATURE-----\",\n+\tNULL\n+};\n+\n static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key);\n \n static struct gpg_format gpg_format[] = {\n \t{\n@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t},\n+\t{\n+\t\t.name = \"ssh\",\n+\t\t.program = \"ssh-keygen\",\n+\t\t.verify_args = ssh_verify_args,\n+\t\t.sigs = ssh_sigs,\n+\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.sign_buffer = sign_buffer_ssh\n+\t},\n };\n \n static struct gpg_format *use_format = &gpg_format[0];\n@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"gpg.x509.program\"))\n \t\tfmtname = \"x509\";\n \n+\tif (!strcmp(var, \"gpg.ssh.program\"))\n+\t\tfmtname = \"ssh\";\n+\n \tif (fmtname) {\n \t\tfmt = get_format_by_name(fmtname);\n \t\treturn git_config_string(&fmt->program, var, value);\n@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig\n \treturn use_format->sign_buffer(buffer, signature, signing_key);\n }\n \n+/*\n+ * Strip CR from the line endings, in case we are on Windows.\n+ * NEEDSWORK: make it trim only CRs before LFs and rename\n+ */\n+static void remove_cr_after(struct strbuf *buffer, size_t offset)\n+{\n+\tsize_t i, j;\n+\n+\tfor (i = j = offset; i < buffer->len; i++) {\n+\t\tif (buffer->buf[i] != '\\r') {\n+\t\t\tif (i != j)\n+\t\t\t\tbuffer->buf[j] = buffer->buf[i];\n+\t\t\tj++;\n+\t\t}\n+\t}\n+\tstrbuf_setlen(buffer, j);\n+}\n+\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t  const char *signing_key)\n {\n \tstruct child_process gpg = CHILD_PROCESS_INIT;\n \tint ret;\n-\tsize_t i, j, bottom;\n+\tsize_t bottom;\n \tstruct strbuf gpg_status = STRBUF_INIT;\n \n \tstrvec_pushl(&gpg.args,\n@@ -494,13 +531,98 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\treturn error(_(\"gpg failed to sign the data\"));\n \n \t/* Strip CR from the line endings, in case we are on Windows. */\n-\tfor (i = j = bottom; i < signature->len; i++)\n-\t\tif (signature->buf[i] != '\\r') {\n-\t\t\tif (i != j)\n-\t\t\t\tsignature->buf[j] = signature->buf[i];\n-\t\t\tj++;\n-\t\t}\n-\tstrbuf_setlen(signature, j);\n+\tremove_cr_after(signature, bottom);\n \n \treturn 0;\n }\n+\n+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n+\t\t\t   const char *signing_key)\n+{\n+\tstruct child_process signer = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tsize_t bottom, keylen;\n+\tstruct strbuf signer_stderr = STRBUF_INIT;\n+\tstruct tempfile *key_file = NULL, *buffer_file = NULL;\n+\tchar *ssh_signing_key_file = NULL;\n+\tstruct strbuf ssh_signature_filename = STRBUF_INIT;\n+\n+\tif (!signing_key || signing_key[0] == '\\0')\n+\t\treturn error(\n+\t\t\t_(\"user.signingkey needs to be set for ssh signing\"));\n+\n+\tif (starts_with(signing_key, \"ssh-\")) {\n+\t\t/* A literal ssh key */\n+\t\tkey_file = mks_tempfile_t(\".git_signing_key_tmpXXXXXX\");\n+\t\tif (!key_file)\n+\t\t\treturn error_errno(\n+\t\t\t\t_(\"could not create temporary file\"));\n+\t\tkeylen = strlen(signing_key);\n+\t\tif (write_in_full(key_file->fd, signing_key, keylen) < 0 ||\n+\t\t    close_tempfile_gently(key_file) < 0) {\n+\t\t\terror_errno(_(\"failed writing ssh signing key to '%s'\"),\n+\t\t\t\t    key_file->filename.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tssh_signing_key_file = strbuf_detach(&key_file->filename, NULL);\n+\t} else {\n+\t\t/* We assume a file */\n+\t\tssh_signing_key_file = expand_user_path(signing_key, 1);\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_signing_buffer_tmpXXXXXX\");\n+\tif (!buffer_file) {\n+\t\terror_errno(_(\"could not create temporary file\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing ssh signing key buffer to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tstrvec_pushl(&signer.args, use_format->program,\n+\t\t     \"-Y\", \"sign\",\n+\t\t     \"-n\", \"git\",\n+\t\t     \"-f\", ssh_signing_key_file,\n+\t\t     buffer_file->filename.buf,\n+\t\t     NULL);\n+\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);\n+\tsigchain_pop(SIGPIPE);\n+\n+\tif (ret) {\n+\t\tif (strstr(signer_stderr.buf, \"usage:\"))\n+\t\t\terror(_(\"ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)\"));\n+\n+\t\terror(\"%s\", signer_stderr.buf);\n+\t\tgoto out;\n+\t}\n+\n+\tbottom = signature->len;\n+\n+\tstrbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);\n+\tstrbuf_addstr(&ssh_signature_filename, \".sig\");\n+\tif (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {\n+\t\terror_errno(\n+\t\t\t_(\"failed reading ssh signing data buffer from '%s'\"),\n+\t\t\tssh_signature_filename.buf);\n+\t}\n+\tunlink_or_warn(ssh_signature_filename.buf);\n+\n+\t/* Strip CR from the line endings, in case we are on Windows. */\n+\tremove_cr_after(signature, bottom);\n+\n+out:\n+\tif (key_file)\n+\t\tdelete_tempfile(&key_file);\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&signer_stderr);\n+\tstrbuf_release(&ssh_signature_filename);\n+\tFREE_AND_NULL(ssh_signing_key_file);\n+\treturn ret;\n+}\n-- \ngitgitgadget\n\n"},{"id":"435470","messageId":"8c430fc7a1b0bc60911e3fe338b094abc09b4ef9.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 4/9] ssh signing: retrieve a default key from ssh-agent","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:37Z","receivedAt":"2021-09-10T20:07:55Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nIf user.signingkey is not set and a ssh signature is requested we call\ngpg.ssh.defaultKeyCommand (typically \"ssh-add -L\") and use the first key we get\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt  |  6 +++\n Documentation/config/user.txt |  4 +-\n gpg-interface.c               | 70 ++++++++++++++++++++++++++++++++++-\n 3 files changed, 77 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex 88531b15f0f..9b95dd280c3 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -33,3 +33,9 @@ gpg.minTrustLevel::\n * `marginal`\n * `fully`\n * `ultimate`\n+\n+gpg.ssh.defaultKeyCommand:\n+\tThis command that will be run when user.signingkey is not set and a ssh\n+\tsignature is requested. On successful exit a valid ssh public key is\n+\texpected in the\tfirst line of its output. To automatically use the first\n+\tavailable key from your ssh-agent set this to \"ssh-add -L\".\ndiff --git a/Documentation/config/user.txt b/Documentation/config/user.txt\nindex 2155128957c..ad78dce9ecb 100644\n--- a/Documentation/config/user.txt\n+++ b/Documentation/config/user.txt\n@@ -40,4 +40,6 @@ user.signingKey::\n \tkey (e.g.: \"ssh-rsa XXXXXX identifier\") or a file which contains it and\n \tcorresponds to the private key used for signing. The private key\n \tneeds to be available via ssh-agent. Alternatively it can be set to\n-\ta file containing a private key directly.\n+\ta file containing a private key directly. If not set git will call\n+\tgpg.ssh.defaultKeyCommand (e.g.: \"ssh-add -L\") and try to use the first\n+\tkey available.\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 7ca682ac6d6..3a0cca1b1d2 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -6,8 +6,10 @@\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n+#include \"alias.h\"\n \n static char *configured_signing_key;\n+static const char *ssh_default_key_command;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -21,6 +23,7 @@ struct gpg_format {\n \t\t\t\t    size_t signature_size);\n \tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n+\tconst char *(*get_default_key)(void);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -56,6 +59,8 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n \n+static const char *get_default_ssh_signing_key(void);\n+\n static struct gpg_format gpg_format[] = {\n \t{\n \t\t.name = \"openpgp\",\n@@ -64,6 +69,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.sigs = openpgp_sigs,\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n+\t\t.get_default_key = NULL,\n \t},\n \t{\n \t\t.name = \"x509\",\n@@ -72,6 +78,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.sigs = x509_sigs,\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n+\t\t.get_default_key = NULL,\n \t},\n \t{\n \t\t.name = \"ssh\",\n@@ -79,7 +86,8 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_args = ssh_verify_args,\n \t\t.sigs = ssh_sigs,\n \t\t.verify_signed_buffer = NULL, /* TODO */\n-\t\t.sign_buffer = sign_buffer_ssh\n+\t\t.sign_buffer = sign_buffer_ssh,\n+\t\t.get_default_key = get_default_ssh_signing_key,\n \t},\n };\n \n@@ -453,6 +461,12 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.defaultkeycommand\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_string(&ssh_default_key_command, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \n@@ -470,11 +484,63 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+/* Returns the first public key from an ssh-agent to use for signing */\n+static const char *get_default_ssh_signing_key(void)\n+{\n+\tstruct child_process ssh_default_key = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf key_stdout = STRBUF_INIT, key_stderr = STRBUF_INIT;\n+\tstruct strbuf **keys;\n+\tchar *key_command = NULL;\n+\tconst char **argv;\n+\tint n;\n+\tchar *default_key = NULL;\n+\n+\tif (!ssh_default_key_command)\n+\t\tdie(_(\"either user.signingkey or gpg.ssh.defaultKeyCommand needs to be configured\"));\n+\n+\tkey_command = xstrdup(ssh_default_key_command);\n+\tn = split_cmdline(key_command, &argv);\n+\n+\tif (n < 0)\n+\t\tdie(\"malformed build-time gpg.ssh.defaultKeyCommand: %s\",\n+\t\t    split_cmdline_strerror(n));\n+\n+\tstrvec_pushv(&ssh_default_key.args, argv);\n+\tret = pipe_command(&ssh_default_key, NULL, 0, &key_stdout, 0,\n+\t\t\t   &key_stderr, 0);\n+\n+\tif (!ret) {\n+\t\tkeys = strbuf_split_max(&key_stdout, '\\n', 2);\n+\t\tif (keys[0] && starts_with(keys[0]->buf, \"ssh-\")) {\n+\t\t\tdefault_key = strbuf_detach(keys[0], NULL);\n+\t\t} else {\n+\t\t\twarning(_(\"gpg.ssh.defaultKeycommand succeeded but returned no keys: %s %s\"),\n+\t\t\t\tkey_stderr.buf, key_stdout.buf);\n+\t\t}\n+\n+\t\tstrbuf_list_free(keys);\n+\t} else {\n+\t\twarning(_(\"gpg.ssh.defaultKeyCommand failed: %s %s\"),\n+\t\t\tkey_stderr.buf, key_stdout.buf);\n+\t}\n+\n+\tfree(key_command);\n+\tfree(argv);\n+\tstrbuf_release(&key_stdout);\n+\n+\treturn default_key;\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\n \t\treturn configured_signing_key;\n-\treturn git_committer_info(IDENT_STRICT|IDENT_NO_DATE);\n+\tif (use_format->get_default_key) {\n+\t\treturn use_format->get_default_key();\n+\t}\n+\n+\treturn git_committer_info(IDENT_STRICT | IDENT_NO_DATE);\n }\n \n int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)\n-- \ngitgitgadget\n\n"},{"id":"435471","messageId":"0864ed04670c271c3e686cc5301bebd45eae2242.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 5/9] ssh signing: provide a textual signing_key_id","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:38Z","receivedAt":"2021-09-10T20:07:55Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nFor ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.\nIn push certs and textual output we prefer the ssh fingerprint instead.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n gpg-interface.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++\n gpg-interface.h |  6 ++++++\n send-pack.c     |  8 +++----\n 3 files changed, 66 insertions(+), 4 deletions(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 3a0cca1b1d2..0f1c6a02e53 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -24,6 +24,7 @@ struct gpg_format {\n \tint (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n \tconst char *(*get_default_key)(void);\n+\tconst char *(*get_key_id)(void);\n };\n \n static const char *openpgp_verify_args[] = {\n@@ -61,6 +62,8 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \n static const char *get_default_ssh_signing_key(void);\n \n+static const char *get_ssh_key_id(void);\n+\n static struct gpg_format gpg_format[] = {\n \t{\n \t\t.name = \"openpgp\",\n@@ -70,6 +73,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t\t.get_default_key = NULL,\n+\t\t.get_key_id = NULL,\n \t},\n \t{\n \t\t.name = \"x509\",\n@@ -79,6 +83,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = verify_gpg_signed_buffer,\n \t\t.sign_buffer = sign_buffer_gpg,\n \t\t.get_default_key = NULL,\n+\t\t.get_key_id = NULL,\n \t},\n \t{\n \t\t.name = \"ssh\",\n@@ -88,6 +93,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.verify_signed_buffer = NULL, /* TODO */\n \t\t.sign_buffer = sign_buffer_ssh,\n \t\t.get_default_key = get_default_ssh_signing_key,\n+\t\t.get_key_id = get_ssh_key_id,\n \t},\n };\n \n@@ -484,6 +490,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \treturn 0;\n }\n \n+static char *get_ssh_key_fingerprint(const char *signing_key)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tint ret = -1;\n+\tstruct strbuf fingerprint_stdout = STRBUF_INIT;\n+\tstruct strbuf **fingerprint;\n+\n+\t/*\n+\t * With SSH Signing this can contain a filename or a public key\n+\t * For textual representation we usually want a fingerprint\n+\t */\n+\tif (starts_with(signing_key, \"ssh-\")) {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\", \"-\", NULL);\n+\t\tret = pipe_command(&ssh_keygen, signing_key,\n+\t\t\t\t   strlen(signing_key), &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t} else {\n+\t\tstrvec_pushl(&ssh_keygen.args, \"ssh-keygen\", \"-lf\",\n+\t\t\t     configured_signing_key, NULL);\n+\t\tret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,\n+\t\t\t\t   NULL, 0);\n+\t}\n+\n+\tif (!!ret)\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\tfingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);\n+\tif (!fingerprint[1])\n+\t\tdie_errno(_(\"failed to get the ssh fingerprint for key '%s'\"),\n+\t\t\t  signing_key);\n+\n+\treturn strbuf_detach(fingerprint[1], NULL);\n+}\n+\n /* Returns the first public key from an ssh-agent to use for signing */\n static const char *get_default_ssh_signing_key(void)\n {\n@@ -532,6 +573,21 @@ static const char *get_default_ssh_signing_key(void)\n \treturn default_key;\n }\n \n+static const char *get_ssh_key_id(void) {\n+\treturn get_ssh_key_fingerprint(get_signing_key());\n+}\n+\n+/* Returns a textual but unique representation of the signing key */\n+const char *get_signing_key_id(void)\n+{\n+\tif (use_format->get_key_id) {\n+\t\treturn use_format->get_key_id();\n+\t}\n+\n+\t/* GPG/GPGSM only store a key id on this variable */\n+\treturn get_signing_key();\n+}\n+\n const char *get_signing_key(void)\n {\n \tif (configured_signing_key)\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex feac4decf8b..beefacbb1e9 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,\n int git_gpg_config(const char *, const char *, void *);\n void set_signing_key(const char *);\n const char *get_signing_key(void);\n+\n+/*\n+ * Returns a textual unique representation of the signing key in use\n+ * Either a GPG KeyID or a SSH Key Fingerprint\n+ */\n+const char *get_signing_key_id(void);\n int check_signature(const char *payload, size_t plen,\n \t\t    const char *signature, size_t slen,\n \t\t    struct signature_check *sigc);\ndiff --git a/send-pack.c b/send-pack.c\nindex b3a495b7b19..bc0fcdbb000 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,\n {\n \tconst struct ref *ref;\n \tstruct string_list_item *item;\n-\tchar *signing_key = xstrdup(get_signing_key());\n+\tchar *signing_key_id = xstrdup(get_signing_key_id());\n \tconst char *cp, *np;\n \tstruct strbuf cert = STRBUF_INIT;\n \tint update_seen = 0;\n \n \tstrbuf_addstr(&cert, \"certificate version 0.1\\n\");\n-\tstrbuf_addf(&cert, \"pusher %s \", signing_key);\n+\tstrbuf_addf(&cert, \"pusher %s \", signing_key_id);\n \tdatestamp(&cert);\n \tstrbuf_addch(&cert, '\\n');\n \tif (args->url && *args->url) {\n@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tif (!update_seen)\n \t\tgoto free_return;\n \n-\tif (sign_buffer(&cert, &cert, signing_key))\n+\tif (sign_buffer(&cert, &cert, get_signing_key()))\n \t\tdie(_(\"failed to sign the push certificate\"));\n \n \tpacket_buf_write(req_buf, \"push-cert%c%s\", 0, cap_string);\n@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,\n \tpacket_buf_write(req_buf, \"push-cert-end\\n\");\n \n free_return:\n-\tfree(signing_key);\n+\tfree(signing_key_id);\n \tstrbuf_release(&cert);\n \treturn update_seen;\n }\n-- \ngitgitgadget\n\n"},{"id":"435472","messageId":"cfd66180249e6d2c817dd187efd71f3f48b6dbd1.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 6/9] ssh signing: verify signatures using ssh-keygen","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:39Z","receivedAt":"2021-09-10T20:07:57Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nTo verify a ssh signature we first call ssh-keygen -Y find-principal to\nlook up the signing principal by their public key from the\nallowedSignersFile. If the key is found then we do a verify. Otherwise\nwe only validate the signature but can not verify the signers identity.\n\nVerification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) \"ALLOWED\nSIGNERS\") which contains valid public keys and a principal (usually\nuser@domain). Depending on the environment this file can be managed by\nthe individual developer or for example generated by the central\nrepository server from known ssh keys with push access. This file is usually\nstored outside the repository, but if the repository only allows signed\ncommits/pushes, the user might choose to store it in the repository.\n\nTo revoke a key put the public key without the principal prefix into\ngpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)\n\"KEY REVOCATION LISTS\"). The same considerations about who to trust for\nverification as with the allowedSignersFile apply.\n\nUsing SSH CA Keys with these files is also possible. Add\n\"cert-authority\" as key option between the principal and the key to mark\nit as a CA and all keys signed by it as valid for this CA.\nSee \"CERTIFICATES\" in ssh-keygen(1).\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n Documentation/config/gpg.txt |  35 ++++++\n builtin/receive-pack.c       |   4 +\n gpg-interface.c              | 215 ++++++++++++++++++++++++++++++++++-\n 3 files changed, 252 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt\nindex 9b95dd280c3..51a756b2f15 100644\n--- a/Documentation/config/gpg.txt\n+++ b/Documentation/config/gpg.txt\n@@ -39,3 +39,38 @@ gpg.ssh.defaultKeyCommand:\n \tsignature is requested. On successful exit a valid ssh public key is\n \texpected in the\tfirst line of its output. To automatically use the first\n \tavailable key from your ssh-agent set this to \"ssh-add -L\".\n+\n+gpg.ssh.allowedSignersFile::\n+\tA file containing ssh public keys which you are willing to trust.\n+\tThe file consists of one or more lines of principals followed by an ssh\n+\tpublic key.\n+\te.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...\n+\tSee ssh-keygen(1) \"ALLOWED SIGNERS\" for details.\n+\tThe principal is only used to identify the key and is available when\n+\tverifying a signature.\n++\n+SSH has no concept of trust levels like gpg does. To be able to differentiate\n+between valid signatures and trusted signatures the trust level of a signature\n+verification is set to `fully` when the public key is present in the allowedSignersFile.\n+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.\n+Otherwise valid but untrusted signatures will still verify but show no principal\n+name of the signer.\n++\n+This file can be set to a location outside of the repository and every developer\n+maintains their own trust store. A central repository server could generate this\n+file automatically from ssh keys with push access to verify the code against.\n+In a corporate setting this file is probably generated at a global location\n+from automation that already handles developer ssh keys.\n++\n+A repository that only allows signed commits can store the file\n+in the repository itself using a path relative to the top-level of the working tree.\n+This way only committers with an already valid key can add or change keys in the keyring.\n++\n+Using a SSH CA key with the cert-authority option\n+(see ssh-keygen(1) \"CERTIFICATES\") is also valid.\n+\n+gpg.ssh.revocationFile::\n+\tEither a SSH KRL or a list of revoked public keys (without the principal prefix).\n+\tSee ssh-keygen(1) for details.\n+\tIf a public key is found in this file then it will always be treated\n+\tas having trust level \"never\" and signatures will show as invalid.\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 2d1f97e1ca7..05dc8e160f8 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -131,6 +131,10 @@ static int receive_pack_config(const char *var, const char *value, void *cb)\n {\n \tint status = parse_hide_refs_config(var, value, \"receive\");\n \n+\tif (status)\n+\t\treturn status;\n+\n+\tstatus = git_gpg_config(var, value, NULL);\n \tif (status)\n \t\treturn status;\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0f1c6a02e53..433482307c0 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -3,13 +3,14 @@\n #include \"config.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n+#include \"dir.h\"\n #include \"gpg-interface.h\"\n #include \"sigchain.h\"\n #include \"tempfile.h\"\n #include \"alias.h\"\n \n static char *configured_signing_key;\n-static const char *ssh_default_key_command;\n+static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;\n static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;\n \n struct gpg_format {\n@@ -55,6 +56,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \t\t\t\t    struct gpg_format *fmt, const char *payload,\n \t\t\t\t    size_t payload_size, const char *signature,\n \t\t\t\t    size_t signature_size);\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size);\n static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,\n \t\t\t   const char *signing_key);\n static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n@@ -90,7 +95,7 @@ static struct gpg_format gpg_format[] = {\n \t\t.program = \"ssh-keygen\",\n \t\t.verify_args = ssh_verify_args,\n \t\t.sigs = ssh_sigs,\n-\t\t.verify_signed_buffer = NULL, /* TODO */\n+\t\t.verify_signed_buffer = verify_ssh_signed_buffer,\n \t\t.sign_buffer = sign_buffer_ssh,\n \t\t.get_default_key = get_default_ssh_signing_key,\n \t\t.get_key_id = get_ssh_key_id,\n@@ -357,6 +362,200 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \treturn ret;\n }\n \n+static void parse_ssh_output(struct signature_check *sigc)\n+{\n+\tconst char *line, *principal, *search;\n+\tchar *key = NULL;\n+\n+\t/*\n+\t * ssh-keygen output should be:\n+\t * Good \"git\" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT\n+\t *\n+\t * or for valid but unknown keys:\n+\t * Good \"git\" signature with RSA key SHA256:FINGERPRINT\n+\t *\n+\t * Note that \"PRINCIPAL\" can contain whitespace, \"RSA\" and\n+\t * \"SHA256\" part could be a different token that names of\n+\t * the algorithms used, and \"FINGERPRINT\" is a hexadecimal\n+\t * string.  By finding the last occurence of \" with \", we can\n+\t * reliably parse out the PRINCIPAL.\n+\t */\n+\tsigc->result = 'B';\n+\tsigc->trust_level = TRUST_NEVER;\n+\n+\tline = xmemdupz(sigc->output, strcspn(sigc->output, \"\\n\"));\n+\n+\tif (skip_prefix(line, \"Good \\\"git\\\" signature for \", &line)) {\n+\t\t/* Valid signature and known principal */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_FULLY;\n+\n+\t\t/* Search for the last \"with\" to get the full principal */\n+\t\tprincipal = line;\n+\t\tdo {\n+\t\t\tsearch = strstr(line, \" with \");\n+\t\t\tif (search)\n+\t\t\t\tline = search + 1;\n+\t\t} while (search != NULL);\n+\t\tsigc->signer = xmemdupz(principal, line - principal - 1);\n+\t} else if (skip_prefix(line, \"Good \\\"git\\\" signature with \", &line)) {\n+\t\t/* Valid signature, but key unknown */\n+\t\tsigc->result = 'G';\n+\t\tsigc->trust_level = TRUST_UNDEFINED;\n+\t} else {\n+\t\treturn;\n+\t}\n+\n+\tkey = strstr(line, \"key\");\n+\tif (key) {\n+\t\tsigc->fingerprint = xstrdup(strstr(line, \"key\") + 4);\n+\t\tsigc->key = xstrdup(sigc->fingerprint);\n+\t} else {\n+\t\t/*\n+\t\t * Output did not match what we expected\n+\t\t * Treat the signature as bad\n+\t\t */\n+\t\tsigc->result = 'B';\n+\t}\n+}\n+\n+static int verify_ssh_signed_buffer(struct signature_check *sigc,\n+\t\t\t\t    struct gpg_format *fmt, const char *payload,\n+\t\t\t\t    size_t payload_size, const char *signature,\n+\t\t\t\t    size_t signature_size)\n+{\n+\tstruct child_process ssh_keygen = CHILD_PROCESS_INIT;\n+\tstruct tempfile *buffer_file;\n+\tint ret = -1;\n+\tconst char *line;\n+\tsize_t trust_size;\n+\tchar *principal;\n+\tstruct strbuf ssh_principals_out = STRBUF_INIT;\n+\tstruct strbuf ssh_principals_err = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_out = STRBUF_INIT;\n+\tstruct strbuf ssh_keygen_err = STRBUF_INIT;\n+\n+\tif (!ssh_allowed_signers) {\n+\t\terror(_(\"gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification\"));\n+\t\treturn -1;\n+\t}\n+\n+\tbuffer_file = mks_tempfile_t(\".git_vtag_tmpXXXXXX\");\n+\tif (!buffer_file)\n+\t\treturn error_errno(_(\"could not create temporary file\"));\n+\tif (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||\n+\t    close_tempfile_gently(buffer_file) < 0) {\n+\t\terror_errno(_(\"failed writing detached signature to '%s'\"),\n+\t\t\t    buffer_file->filename.buf);\n+\t\tdelete_tempfile(&buffer_file);\n+\t\treturn -1;\n+\t}\n+\n+\t/* Find the principal from the signers */\n+\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t     \"-Y\", \"find-principals\",\n+\t\t     \"-f\", ssh_allowed_signers,\n+\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t     NULL);\n+\tret = pipe_command(&ssh_keygen, NULL, 0, &ssh_principals_out, 0,\n+\t\t\t   &ssh_principals_err, 0);\n+\tif (ret && strstr(ssh_principals_err.buf, \"usage:\")) {\n+\t\terror(_(\"ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)\"));\n+\t\tgoto out;\n+\t}\n+\tif (ret || !ssh_principals_out.len) {\n+\t\t/*\n+\t\t * We did not find a matching principal in the allowedSigners\n+\t\t * Check without validation\n+\t\t */\n+\t\tchild_process_init(&ssh_keygen);\n+\t\tstrvec_pushl(&ssh_keygen.args, fmt->program,\n+\t\t\t     \"-Y\", \"check-novalidate\",\n+\t\t\t     \"-n\", \"git\",\n+\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t     NULL);\n+\t\tpipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\n+\t\t/*\n+\t\t * Fail on unknown keys\n+\t\t * we still call check-novalidate to display the signature info\n+\t\t */\n+\t\tret = -1;\n+\t} else {\n+\t\t/* Check every principal we found (one per line) */\n+\t\tfor (line = ssh_principals_out.buf; *line;\n+\t\t     line = strchrnul(line + 1, '\\n')) {\n+\t\t\twhile (*line == '\\n')\n+\t\t\t\tline++;\n+\t\t\tif (!*line)\n+\t\t\t\tbreak;\n+\n+\t\t\ttrust_size = strcspn(line, \"\\n\");\n+\t\t\tprincipal = xmemdupz(line, trust_size);\n+\n+\t\t\tchild_process_init(&ssh_keygen);\n+\t\t\tstrbuf_release(&ssh_keygen_out);\n+\t\t\tstrbuf_release(&ssh_keygen_err);\n+\t\t\tstrvec_push(&ssh_keygen.args, fmt->program);\n+\t\t\t/*\n+\t\t\t * We found principals\n+\t\t\t * Try with each until we find a match\n+\t\t\t */\n+\t\t\tstrvec_pushl(&ssh_keygen.args, \"-Y\", \"verify\",\n+\t\t\t\t     \"-n\", \"git\",\n+\t\t\t\t     \"-f\", ssh_allowed_signers,\n+\t\t\t\t     \"-I\", principal,\n+\t\t\t\t     \"-s\", buffer_file->filename.buf,\n+\t\t\t\t     NULL);\n+\n+\t\t\tif (ssh_revocation_file) {\n+\t\t\t\tif (file_exists(ssh_revocation_file)) {\n+\t\t\t\t\tstrvec_pushl(&ssh_keygen.args, \"-r\",\n+\t\t\t\t\t\t     ssh_revocation_file, NULL);\n+\t\t\t\t} else {\n+\t\t\t\t\twarning(_(\"ssh signing revocation file configured but not found: %s\"),\n+\t\t\t\t\t\tssh_revocation_file);\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tsigchain_push(SIGPIPE, SIG_IGN);\n+\t\t\tret = pipe_command(&ssh_keygen, payload, payload_size,\n+\t\t\t\t\t   &ssh_keygen_out, 0, &ssh_keygen_err, 0);\n+\t\t\tsigchain_pop(SIGPIPE);\n+\n+\t\t\tFREE_AND_NULL(principal);\n+\n+\t\t\tif (!ret)\n+\t\t\t\tret = !starts_with(ssh_keygen_out.buf, \"Good\");\n+\n+\t\t\tif (!ret)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tsigc->payload = xmemdupz(payload, payload_size);\n+\tstrbuf_stripspace(&ssh_keygen_out, 0);\n+\tstrbuf_stripspace(&ssh_keygen_err, 0);\n+\t/* Add stderr outputs to show the user actual ssh-keygen errors */\n+\tstrbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);\n+\tstrbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);\n+\tsigc->output = strbuf_detach(&ssh_keygen_out, NULL);\n+\tsigc->gpg_status = xstrdup(sigc->output);\n+\n+\tparse_ssh_output(sigc);\n+\n+out:\n+\tif (buffer_file)\n+\t\tdelete_tempfile(&buffer_file);\n+\tstrbuf_release(&ssh_principals_out);\n+\tstrbuf_release(&ssh_principals_err);\n+\tstrbuf_release(&ssh_keygen_out);\n+\tstrbuf_release(&ssh_keygen_err);\n+\n+\treturn ret;\n+}\n+\n int check_signature(const char *payload, size_t plen, const char *signature,\n \tsize_t slen, struct signature_check *sigc)\n {\n@@ -473,6 +672,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)\n \t\treturn git_config_string(&ssh_default_key_command, var, value);\n \t}\n \n+\tif (!strcmp(var, \"gpg.ssh.allowedsignersfile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_pathname(&ssh_allowed_signers, var, value);\n+\t}\n+\n+\tif (!strcmp(var, \"gpg.ssh.revocationfile\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\treturn git_config_pathname(&ssh_revocation_file, var, value);\n+\t}\n+\n \tif (!strcmp(var, \"gpg.program\") || !strcmp(var, \"gpg.openpgp.program\"))\n \t\tfmtname = \"openpgp\";\n \n-- \ngitgitgadget\n\n"},{"id":"435473","messageId":"c8e21dc97f1bd19f57876a72604d3be098afa10a.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 7/9] ssh signing: duplicate t7510 tests for commits","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:40Z","receivedAt":"2021-09-10T20:08:01Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++\n 1 file changed, 398 insertions(+)\n create mode 100755 t/t7528-signed-commit-ssh.sh\n\ndiff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh\nnew file mode 100755\nindex 00000000000..badf3ed3204\n--- /dev/null\n+++ b/t/t7528-signed-commit-ssh.sh\n@@ -0,0 +1,398 @@\n+#!/bin/sh\n+\n+test_description='ssh signed commit tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed commits' '\n+\ttest_oid_cache <<-\\EOF &&\n+\theader sha1:gpgsig\n+\theader sha256:gpgsig-sha256\n+\tEOF\n+\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -S -m initial &&\n+\tgit tag initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -S -m second &&\n+\tgit tag second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -m \"fourth unsigned\" &&\n+\tgit tag fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag fourth-signed &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 5 >file && test_tick && git commit -a -m \"fifth signed\" &&\n+\tgit tag fifth-signed &&\n+\n+\tgit config commit.gpgsign false &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag sixth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 7 >file && test_tick && git commit -a -m \"seventh\" --no-gpg-sign &&\n+\tgit tag seventh-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&\n+\tgit tag seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth -S\"${GPGSSH_KEY_UNTRUSTED}\" &&\n+\tgit tag eighth-signed-alt &&\n+\n+\t# commit.gpgsign is still on but this must not be signed\n+\techo 9 | git commit-tree HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag ninth-unsigned $(cat oid) &&\n+\t# explicit -S of course must sign.\n+\techo 10 | git commit-tree -S HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag tenth-signed $(cat oid) &&\n+\n+\t# --gpg-sign[=<key-id>] must sign.\n+\techo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag eleventh-signed $(cat oid) &&\n+\techo 12 | git commit-tree --gpg-sign=\"${GPGSSH_KEY_UNTRUSTED}\" HEAD^{tree} >oid &&\n+\ttest_line_count = 1 oid &&\n+\tgit tag twelfth-signed-alt $(cat oid)\n+'\n+\n+test_expect_success GPGSSH 'verify and show signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.mintrustlevel UNDEFINED &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed \\\n+\t\t\tfifth-signed sixth-signed seventh-signed tenth-signed \\\n+\t\t\televenth-signed\n+\t\tdo\n+\t\t\tgit verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned \\\n+\t\t\tseventh-unsigned ninth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit $commit &&\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt twelfth-signed-alt\n+\t\tdo\n+\t\t\tgit show --pretty=short --show-signature $commit >actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${GPGSSH_KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_must_fail git verify-commit eighth-signed-alt 2>actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\tgrep \"${GPGSSH_KEY_NOT_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel fully &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config gpg.minTrustLevel marginal &&\n+\tgit verify-commit sixth-signed\n+'\n+\n+test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '\n+\ttest_config gpg.minTrustLevel ultimate &&\n+\ttest_must_fail git verify-commit eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor commit in eighth-signed-alt\n+\t\tdo\n+\t\t\ttest_must_fail git verify-commit --raw $commit 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $commit OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'proper header is used for hash algorithm' '\n+\tgit cat-file commit fourth-signed >output &&\n+\tgrep \"^$(test_oid header) -----BEGIN SSH SIGNATURE-----\" output\n+'\n+\n+test_expect_success GPGSSH 'show signed commit with signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit show -s initial >commit &&\n+\tgit show -s --show-signature initial >show &&\n+\tgit verify-commit -v initial >verify.1 2>verify.2 &&\n+\tgit cat-file commit initial >cat &&\n+\tgrep -v -e \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.commit &&\n+\tgrep -e \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" -e \"Warning: \" show >show.gpg &&\n+\tgrep -v \"^ \" cat | grep -v \"^gpgsig.* \" >cat.commit &&\n+\ttest_cmp show.commit commit &&\n+\ttest_cmp show.gpg verify.2 &&\n+\ttest_cmp cat.commit verify.1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tsed -e \"s/^seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t commit forged1 >forged1.commit &&\n+\ttest_must_fail git verify-commit $(cat forged1.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&\n+\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'detect fudged signature with NUL' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file commit seventh-signed >raw &&\n+\tcat raw >forged2 &&\n+\techo Qwik | tr \"Q\" \"\\000\" >>forged2 &&\n+\tgit hash-object -w -t commit forged2 >forged2.commit &&\n+\ttest_must_fail git verify-commit $(cat forged2.commit) &&\n+\tgit show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&\n+\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual2 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual2\n+'\n+\n+test_expect_success GPGSSH 'amending already signed commit' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit checkout fourth-signed^0 &&\n+\tgit commit --amend -S --no-edit &&\n+\tgit verify-commit HEAD &&\n+\tgit show -s --show-signature HEAD >actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual\n+'\n+\n+test_expect_success GPGSSH 'show good signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tG\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show bad signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\tB\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat forged1.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with custom format' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tU\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tundefined\n+\tFINGERPRINT\n+\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" eighth-signed-alt >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_UNTRUSTED}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect.tmpl <<-\\EOF &&\n+\tfully\n+\tFINGERPRINT\n+\tprincipal with number 1\n+\tFINGERPRINT\n+\n+\tEOF\n+\tgit log -1 --format=\"%GT%n%GK%n%GS%n%GF%n%GP\" sixth-signed >actual &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tsed \"s|FINGERPRINT|$FINGERPRINT|g\" expect.tmpl >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'show lack of signature with custom format' '\n+\tcat >expect <<-\\EOF &&\n+\tN\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" seventh-unsigned >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttest_config log.showsignature true &&\n+\tgit show initial >actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n+test_expect_success GPGSSH 'check config gpg.format values' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\ttest_config gpg.format ssh &&\n+\tgit commit -S --amend -m \"success\" &&\n+\ttest_config gpg.format OpEnPgP &&\n+\ttest_must_fail git commit -S --amend -m \"fail\"\n+'\n+\n+test_expect_failure GPGSSH 'detect fudged commit with double signature (TODO)' '\n+\tsed -e \"/gpgsig/,/END PGP/d\" forged1 >double-base &&\n+\tsed -n -e \"/gpgsig/,/END PGP/p\" forged1 | \\\n+\t\tsed -e \"s/^$(test_oid header)//;s/^ //\" | gpg --dearmor >double-sig1.sig &&\n+\tgpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&\n+\tcat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&\n+\tsed -e \"s/^\\(-.*\\)ARMORED FILE/\\1SIGNATURE/;1s/^/$(test_oid header) /;2,\\$s/^/ /\" \\\n+\t\tdouble-combined.asc > double-gpgsig &&\n+\tsed -e \"/committer/r double-gpgsig\" double-base >double-commit &&\n+\tgit hash-object -w -t commit double-commit >double-commit.commit &&\n+\ttest_must_fail git verify-commit $(cat double-commit.commit) &&\n+\tgit show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&\n+\tgrep \"BAD signature from\" double-actual &&\n+\tgrep \"Good signature from\" double-actual\n+'\n+\n+test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '\n+\tcat >expect <<-\\EOF &&\n+\tE\n+\n+\n+\n+\n+\tEOF\n+\tgit log -1 --format=\"%G?%n%GK%n%GS%n%GF%n%GP\" $(cat double-commit.commit) >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '\n+\tgit init multiply-signed &&\n+\tcd multiply-signed &&\n+\ttest_commit first &&\n+\techo 1 >second &&\n+\tgit add second &&\n+\ttree=$(git write-tree) &&\n+\tparent=$(git rev-parse HEAD^{commit}) &&\n+\tgit commit --gpg-sign -m second &&\n+\tgit cat-file commit HEAD &&\n+\t# Avoid trailing whitespace.\n+\tsed -e \"s/^Q//\" -e \"s/^Z/ /\" >commit <<-EOF &&\n+\tQtree $tree\n+\tQparent $parent\n+\tQauthor A U Thor <author@example.com> 1112912653 -0700\n+\tQcommitter C O Mitter <committer@example.com> 1112912653 -0700\n+\tQgpgsig -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n+\tQ AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n+\tQ =tQ0N\n+\tQ -----END PGP SIGNATURE-----\n+\tQgpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n+\tQZ\n+\tQ iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n+\tQ QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n+\tQ AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n+\tQ =pIwP\n+\tQ -----END PGP SIGNATURE-----\n+\tQ\n+\tQsecond\n+\tEOF\n+\thead=$(git hash-object -t commit -w commit) &&\n+\tgit reset --hard $head &&\n+\tgit verify-commit $head 2>actual &&\n+\tgrep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"435474","messageId":"b66e3e0284cd83ca828ab8f95f2b53e15edf8bf5.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 8/9] ssh signing: tests for logs, tags & push certs","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:41Z","receivedAt":"2021-09-10T20:08:02Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t4202-log.sh                   |  23 +++++\n t/t5534-push-signed.sh           | 101 +++++++++++++++++++\n t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++\n 3 files changed, 285 insertions(+)\n create mode 100755 t/t7031-verify-tag-signed-ssh.sh\n\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 9dfead936b7..6a650dacd6e 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' '\n \tgit commit -S -m signed_commit\n '\n \n+test_expect_success GPGSSH 'setup sshkey signed branch' '\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\ttest_when_finished \"git reset --hard && git checkout main\" &&\n+\tgit checkout -b signed-ssh main &&\n+\techo foo >foo &&\n+\tgit add foo &&\n+\tgit commit -S -m signed_commit\n+'\n+\n test_expect_success GPGSM 'log x509 fingerprint' '\n \techo \"F8BF62E0693D0694816377099909C779FA23FD65 | \" >expect &&\n \tgit log -n1 --format=\"%GF | %GP\" signed-x509 >actual &&\n@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPGSSH 'log ssh key fingerprint' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tssh-keygen -lf  \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2\\\" | \\\"}\" >expect &&\n+\tgit log -n1 --format=\"%GF | %GP\" signed-ssh >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature' '\n \tgit log --graph --show-signature -n1 signed >actual &&\n \tgrep \"^| gpg: Signature made\" actual &&\n@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' '\n \tgrep \"^| gpgsm: Good signature\" actual\n '\n \n+test_expect_success GPGSSH 'log --graph --show-signature ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit log --graph --show-signature -n1 signed-ssh >actual &&\n+\tgrep \"${GOOD_SIGNATURE_TRUSTED}\" actual\n+'\n+\n test_expect_success GPG 'log --graph --show-signature for merged tag' '\n \ttest_when_finished \"git reset --hard && git checkout main\" &&\n \tgit checkout -b plain main &&\ndiff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh\nindex bba768f5ded..24d374adbae 100755\n--- a/t/t5534-push-signed.sh\n+++ b/t/t5534-push-signed.sh\n@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'ssh signed push sends push certificate' '\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'inconsistent push options in signed push not allowed' '\n \t# First, invoke receive-pack with dummy input to obtain its preamble.\n \tprepare_dst &&\n@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '\n \ttest_cmp expect dst/push-cert-status\n '\n \n+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '\n+\ttest_config gpg.format ssh &&\n+\tprepare_dst &&\n+\tmkdir -p dst/.git/hooks &&\n+\tgit -C dst config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit -C dst config receive.certnonceseed sekrit &&\n+\twrite_script dst/.git/hooks/post-receive <<-\\EOF &&\n+\t# discard the update list\n+\tcat >/dev/null\n+\t# record the push certificate\n+\tif test -n \"${GIT_PUSH_CERT-}\"\n+\tthen\n+\t\tgit cat-file blob $GIT_PUSH_CERT >../push-cert\n+\tfi &&\n+\n+\tcat >../push-cert-status <<E_O_F\n+\tSIGNER=${GIT_PUSH_CERT_SIGNER-nobody}\n+\tKEY=${GIT_PUSH_CERT_KEY-nokey}\n+\tSTATUS=${GIT_PUSH_CERT_STATUS-nostatus}\n+\tNONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}\n+\tNONCE=${GIT_PUSH_CERT_NONCE-nononce}\n+\tE_O_F\n+\n+\tEOF\n+\n+\ttest_config user.email hasnokey@nowhere.com &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"\" &&\n+\t(\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git push --signed dst noop ff +noff\n+\t) &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\tFINGERPRINT=$(ssh-keygen -lf \"${GPGSSH_KEY_PRIMARY}\" | awk \"{print \\$2;}\") &&\n+\tgit push --signed dst noop ff +noff &&\n+\n+\t(\n+\t\tcat <<-\\EOF &&\n+\t\tSIGNER=principal with number 1\n+\t\tKEY=FINGERPRINT\n+\t\tSTATUS=G\n+\t\tNONCE_STATUS=OK\n+\t\tEOF\n+\t\tsed -n -e \"s/^nonce /NONCE=/p\" -e \"/^$/q\" dst/push-cert\n+\t) | sed -e \"s|FINGERPRINT|$FINGERPRINT|\" >expect &&\n+\n+\tnoop=$(git rev-parse noop) &&\n+\tff=$(git rev-parse ff) &&\n+\tnoff=$(git rev-parse noff) &&\n+\tgrep \"$noop $ff refs/heads/ff\" dst/push-cert &&\n+\tgrep \"$noop $noff refs/heads/noff\" dst/push-cert &&\n+\ttest_cmp expect dst/push-cert-status\n+'\n+\n test_expect_success GPG 'failed atomic push does not execute GPG' '\n \tprepare_dst &&\n \tgit -C dst config receive.certnonceseed sekrit &&\ndiff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh\nnew file mode 100755\nindex 00000000000..06c9dd6c933\n--- /dev/null\n+++ b/t/t7031-verify-tag-signed-ssh.sh\n@@ -0,0 +1,161 @@\n+#!/bin/sh\n+\n+test_description='signed tag tests'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success GPGSSH 'create signed tags ssh' '\n+\ttest_when_finished \"test_unconfig commit.gpgsign\" &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\n+\techo 1 >file && git add file &&\n+\ttest_tick && git commit -m initial &&\n+\tgit tag -s -m initial initial &&\n+\tgit branch side &&\n+\n+\techo 2 >file && test_tick && git commit -a -m second &&\n+\tgit tag -s -m second second &&\n+\n+\tgit checkout side &&\n+\techo 3 >elif && git add elif &&\n+\ttest_tick && git commit -m \"third on side\" &&\n+\n+\tgit checkout main &&\n+\ttest_tick && git merge -S side &&\n+\tgit tag -s -m merge merge &&\n+\n+\techo 4 >file && test_tick && git commit -a -S -m \"fourth unsigned\" &&\n+\tgit tag -a -m fourth-unsigned fourth-unsigned &&\n+\n+\ttest_tick && git commit --amend -S -m \"fourth signed\" &&\n+\tgit tag -s -m fourth fourth-signed &&\n+\n+\techo 5 >file && test_tick && git commit -a -m \"fifth\" &&\n+\tgit tag fifth-unsigned &&\n+\n+\tgit config commit.gpgsign true &&\n+\techo 6 >file && test_tick && git commit -a -m \"sixth\" &&\n+\tgit tag -a -m sixth sixth-unsigned &&\n+\n+\ttest_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&\n+\tgit tag -m seventh -s seventh-signed &&\n+\n+\techo 8 >file && test_tick && git commit -a -m eighth &&\n+\tgit tag -u\"${GPGSSH_KEY_UNTRUSTED}\" -m eighth eighth-signed-alt\n+'\n+\n+test_expect_success GPGSSH 'verify and show ssh signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\tgrep \"${GPGSSH_KEY_NOT_TRUSTED}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'detect fudged ssh signature' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit cat-file tag seventh-signed >raw &&\n+\tsed -e \"/^tag / s/seventh/7th forged/\" raw >forged1 &&\n+\tgit hash-object -w -t tag forged1 >forged1.tag &&\n+\ttest_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&\n+\tgrep \"${GPGSSH_BAD_SIGNATURE}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual1 &&\n+\t! grep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual1\n+'\n+\n+test_expect_success GPGSSH 'verify ssh signatures with --raw' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\t(\n+\t\tfor tag in initial second merge fourth-signed sixth-signed seventh-signed\n+\t\tdo\n+\t\t\tgit verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in fourth-unsigned fifth-unsigned sixth-unsigned\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t) &&\n+\t(\n+\t\tfor tag in eighth-signed-alt\n+\t\tdo\n+\t\t\ttest_must_fail git verify-tag --raw $tag 2>actual &&\n+\t\t\tgrep \"${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}\" actual &&\n+\t\t\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\t\t\techo $tag OK || exit 1\n+\t\tdone\n+\t)\n+'\n+\n+test_expect_success GPGSSH 'verify signatures with --raw ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit verify-tag --raw sixth-signed 2>actual &&\n+\tgrep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual &&\n+\t! grep \"${GPGSSH_BAD_SIGNATURE}\" actual &&\n+\techo sixth-signed OK\n+'\n+\n+test_expect_success GPGSSH 'verify multiple tags ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\ttags=\"seventh-signed sixth-signed\" &&\n+\tfor i in $tags\n+\tdo\n+\t\tgit verify-tag -v --raw $i || return 1\n+\tdone >expect.stdout 2>expect.stderr.1 &&\n+\tgrep \"^${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" <expect.stderr.1 >expect.stderr &&\n+\tgit verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&\n+\tgrep \"^${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" <actual.stderr.1 >actual.stderr &&\n+\ttest_cmp expect.stdout actual.stdout &&\n+\ttest_cmp expect.stderr actual.stderr\n+'\n+\n+test_expect_success GPGSSH 'verifying tag with --format - ssh' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tcat >expect <<-\\EOF &&\n+\ttagname : fourth-signed\n+\tEOF\n+\tgit verify-tag --format=\"tagname : %(tag)\" \"fourth-signed\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently - ssh' '\n+\ttest_must_fail git verify-tag --format=\"tagname : %(tag)\" $(cat forged1.tag) >actual-forged &&\n+\ttest_must_be_empty actual-forged\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"435475","messageId":"07afb94ed8336d4ca9de7078d7a6c02b1db8a908.1631304462.git.gitgitgadget@gmail.com","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"[PATCH v8 9/9] ssh signing: test that gpg fails for unknown keys","fromName":"Fabian Stelzer via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2021-09-10T20:07:42Z","receivedAt":"2021-09-10T20:08:08Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"From: Fabian Stelzer <fs@gigacodes.de>\n\nTest that verify-commit/tag will fail when a gpg key is completely\nunknown. To do this we have to generate a key, use it for a signature\nand delete it from our keyring aferwards completely.\n\nSigned-off-by: Fabian Stelzer <fs@gigacodes.de>\n---\n t/t7510-signed-commit.sh | 29 ++++++++++++++++++++++++++++-\n 1 file changed, 28 insertions(+), 1 deletion(-)\n\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 8df5a74f1db..d65a0171f29 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -71,7 +71,25 @@ test_expect_success GPG 'create signed commits' '\n \tgit tag eleventh-signed $(cat oid) &&\n \techo 12 | git commit-tree --gpg-sign=B7227189 HEAD^{tree} >oid &&\n \ttest_line_count = 1 oid &&\n-\tgit tag twelfth-signed-alt $(cat oid)\n+\tgit tag twelfth-signed-alt $(cat oid) &&\n+\n+\tcat >keydetails <<-\\EOF &&\n+\tKey-Type: RSA\n+\tKey-Length: 2048\n+\tSubkey-Type: RSA\n+\tSubkey-Length: 2048\n+\tName-Real: Unknown User\n+\tName-Email: unknown@git.com\n+\tExpire-Date: 0\n+\t%no-ask-passphrase\n+\t%no-protection\n+\tEOF\n+\tgpg --batch --gen-key keydetails &&\n+\techo 13 >file && git commit -a -S\"unknown@git.com\" -m thirteenth &&\n+\tgit tag thirteenth-signed &&\n+\tDELETE_FINGERPRINT=$(gpg -K --with-colons --fingerprint --batch unknown@git.com | grep \"^fpr\" | head -n 1 | awk -F \":\" \"{print \\$10;}\") &&\n+\tgpg --batch --yes --delete-secret-keys $DELETE_FINGERPRINT &&\n+\tgpg --batch --yes --delete-keys unknown@git.com\n '\n \n test_expect_success GPG 'verify and show signatures' '\n@@ -110,6 +128,13 @@ test_expect_success GPG 'verify and show signatures' '\n \t)\n '\n \n+test_expect_success GPG 'verify-commit exits failure on unknown signature' '\n+\ttest_must_fail git verify-commit thirteenth-signed 2>actual &&\n+\t! grep \"Good signature from\" actual &&\n+\t! grep \"BAD signature from\" actual &&\n+\tgrep -q -F -e \"No public key\" -e \"public key not found\" actual\n+'\n+\n test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n \tgit verify-commit eighth-signed-alt 2>actual &&\n \tgrep \"Good signature from\" actual &&\n@@ -338,6 +363,8 @@ test_expect_success GPG 'show double signature with custom format' '\n '\n \n \n+# NEEDSWORK: This test relies on the test_tick commit/author dates from the first\n+# 'create signed commits' test even though it creates its own\n test_expect_success GPG 'verify-commit verifies multiply signed commits' '\n \tgit init multiply-signed &&\n \tcd multiply-signed &&\n-- \ngitgitgadget\n"},{"id":"435477","messageId":"CAPUEspgfRtTNXcVkSQ2FOS87m-jOD_Rx60mH0VP4M58_cHaHug@mail.gmail.com","threadId":"56054","inReplyTo":"532d97e7-8c91-df6a-6d90-70668256f513@gigacodes.de","subject":"Re: [PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Carlo Arenas","fromEmail":"carenas@gmail.com","sentAt":"2021-09-10T20:20:18Z","receivedAt":"2021-09-10T20:20:33Z","isPatch":true,"sender":{"key":"carenas@gmail.com","avatar":"https://avatars.githubusercontent.com/u/76036?v=4"},"body":"ON Fri, Sep 10, 2021 at 12:49 PM Fabian Stelzer <fs@gigacodes.de> wrote:\n>\n> On 10.09.21 20:44, Junio C Hamano wrote:\n>\n> > Fabian Stelzer <fs@gigacodes.de> writes:\n> >\n> >> It it not so much an incompatibility but a hard bug in ssh-keygen of my\n> >> own making :/\n> >> There is nothing we can do on the git side to fix this since the\n> >> find-principal call will always segfault no matter what.\n> > So... we cannot do anythying utnil a corrected OpenSSH is made\n> > available, but once we can link with a corrected one, do we need to\n> > do anything further on the patches in your topic?\n>\n> OpenSSH will probably release a new version in October.\n\nFWIW the crashing bug is only in master (I found it while testing\nOpenBSD 7 beta).\nAFAIK, once that is fixed the suite runs cleanly, but still does not\nwhen run against\nan OpenSSH 4.7 release (hadn't check why, but AFAIK wasn't the crash from what\nI recall)\n\n> I will send a new diff of my patch in a bit after the CI runs are\n> through fixing a bug with some buffers that could sometimes lead to\n> memory corruption (i war releasing a buffer while still iterating over\n> its contents), a small test fix and a minor improvement using\n> git_config_pathname instead of string.\n\nnotice that since your patches are already in next (and I know it is\nlate since I saw\nyour update), you need to send only incremental patches now, instead.\n\nCarlo\n"},{"id":"435478","messageId":"xmqqfsucrxhg.fsf@gitster.g","threadId":"56054","inReplyTo":"pull.1041.v8.git.git.1631304462.gitgitgadget@gmail.com","subject":"Re: [PATCH v8 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-09-10T20:23:23Z","receivedAt":"2021-09-10T20:23:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> v8:\n>\n>  * fixes a bug around find-principals buffer i was releasing while still\n>    iterating over it. Uses separate strbufs now.\n>  * rename a wrong variable in the tests\n>  * use git_config_pathname instead of string where applicable\n\nI guess I'd better kick the topic out of 'next' before doing\nanything else, as it still seems to want to be replaceable\nwholesale.  Somehow I was given a (probably false) impression that\nthe previous one was in a more or less testable shape and we can go\nincremental already, which was why I merged v7 to 'next'.\n\nWill queue later, but may not get around to it today.\n\nThanks.\n"},{"id":"435481","messageId":"ced584ee-4f8b-24d3-1f5b-459c67e75584@gigacodes.de","threadId":"56054","inReplyTo":"xmqqfsucrxhg.fsf@gitster.g","subject":"Re: [PATCH v8 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-09-10T20:48:09Z","receivedAt":"2021-09-10T20:48:21Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 10.09.21 22:23, Junio C Hamano wrote:\n\n> \"Fabian Stelzer via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> v8:\n>>\n>>  * fixes a bug around find-principals buffer i was releasing while still\n>>    iterating over it. Uses separate strbufs now.\n>>  * rename a wrong variable in the tests\n>>  * use git_config_pathname instead of string where applicable\n> I guess I'd better kick the topic out of 'next' before doing\n> anything else, as it still seems to want to be replaceable\n> wholesale.  Somehow I was given a (probably false) impression that\n> the previous one was in a more or less testable shape and we can go\n> incremental already, which was why I merged v7 to 'next'.\n\n\nSorry, i think i'm just not familiar with the process. What do i do when\nthe patch is in next and someone (or myself) find other bugs during testing?\nDo i send a new patch based on \"next\" or update my patchset but not\nsquashing the fixup commits?\n\n\n"},{"id":"435482","messageId":"xmqqbl50rvq3.fsf@gitster.g","threadId":"56054","inReplyTo":"ced584ee-4f8b-24d3-1f5b-459c67e75584@gigacodes.de","subject":"Re: [PATCH v8 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-09-10T21:01:24Z","receivedAt":"2021-09-10T21:01:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Fabian Stelzer <fs@gigacodes.de> writes:\n\n> Sorry, i think i'm just not familiar with the process. What do i do when\n> the patch is in next and someone (or myself) find other bugs during testing?\n> Do i send a new patch based on \"next\" or update my patchset but not\n> squashing the fixup commits?\n\nIn general, you'd send an incremental update on top of what you\nsubmitted and has been queued in my tree so far.  Looking for the\nmerge of the topic from the tip of 'next':\n\n  $ git show -s \"next^{/^Merge branch 'fs/ssh-signing' into next}\" |\n    grep \"^Merge:\"\n  Merge: 348fe07b87 b88bcd013b\n  $ git log --oneline --reverse master..b88bcd013b\n  c222385164 ssh signing: preliminary refactoring and clean-up\n  3a3fdc0b4e ssh signing: add test prereqs\n  c7e2d30efe ssh signing: add ssh key format and signing code\n  5493722122 ssh signing: retrieve a default key from ssh-agent\n  6869f1f60c ssh signing: provide a textual signing_key_id\n  9048bb3c9b ssh signing: verify signatures using ssh-keygen\n  587967698a ssh signing: duplicate t7510 tests for commits\n  52ac6bd36f ssh signing: tests for logs, tags & push certs\n  b88bcd013b ssh signing: test that gpg fails for unknown keys\n\nwe learn that b88bcd013b is the tip, so you'd send follow-up patches\nto either fix a bug that exists in the tree of b88bcd013b, or enhance\na feature on top of the tree of b88bcd013b.\n\nBut since I am already ejecting the previous round out of 'next',\nlet's remember to do so the next time.  We will have to wait until\nmid October (if I recall what I thought I read from you correctly)\nanyway, so until then we can iterate outside the 'next' branch.\n\nThanks.\n"},{"id":"444703","messageId":"211222.86ilvhpbl0.gmgdl@evledraar.gmail.com","threadId":"56054","inReplyTo":"07afb94ed8336d4ca9de7078d7a6c02b1db8a908.1631304462.git.gitgitgadget@gmail.com","subject":"t7510-signed-commit.sh hangs on old gpg, regression in 1bfb57f642d (was: [PATCH v8 9/9] ssh signing: test that gpg fails for unknown keys)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-22T03:18:36Z","receivedAt":"2021-12-22T03:23:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Sep 10 2021, Fabian Stelzer via GitGitGadget wrote:\n\n> From: Fabian Stelzer <fs@gigacodes.de>\n>\n> Test that verify-commit/tag will fail when a gpg key is completely\n> unknown. To do this we have to generate a key, use it for a signature\n> and delete it from our keyring aferwards completely.\n>\n> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n> ---\n>  t/t7510-signed-commit.sh | 29 ++++++++++++++++++++++++++++-\n>  1 file changed, 28 insertions(+), 1 deletion(-)\n>\n> diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\n> index 8df5a74f1db..d65a0171f29 100755\n> --- a/t/t7510-signed-commit.sh\n> +++ b/t/t7510-signed-commit.sh\n> @@ -71,7 +71,25 @@ test_expect_success GPG 'create signed commits' '\n>  \tgit tag eleventh-signed $(cat oid) &&\n>  \techo 12 | git commit-tree --gpg-sign=B7227189 HEAD^{tree} >oid &&\n>  \ttest_line_count = 1 oid &&\n> -\tgit tag twelfth-signed-alt $(cat oid)\n> +\tgit tag twelfth-signed-alt $(cat oid) &&\n> +\n> +\tcat >keydetails <<-\\EOF &&\n> +\tKey-Type: RSA\n> +\tKey-Length: 2048\n> +\tSubkey-Type: RSA\n> +\tSubkey-Length: 2048\n> +\tName-Real: Unknown User\n> +\tName-Email: unknown@git.com\n> +\tExpire-Date: 0\n> +\t%no-ask-passphrase\n> +\t%no-protection\n> +\tEOF\n> +\tgpg --batch --gen-key keydetails &&\n> +\techo 13 >file && git commit -a -S\"unknown@git.com\" -m thirteenth &&\n> +\tgit tag thirteenth-signed &&\n> +\tDELETE_FINGERPRINT=$(gpg -K --with-colons --fingerprint --batch unknown@git.com | grep \"^fpr\" | head -n 1 | awk -F \":\" \"{print \\$10;}\") &&\n> +\tgpg --batch --yes --delete-secret-keys $DELETE_FINGERPRINT &&\n> +\tgpg --batch --yes --delete-keys unknown@git.com\n>  '\n>  \n>  test_expect_success GPG 'verify and show signatures' '\n> @@ -110,6 +128,13 @@ test_expect_success GPG 'verify and show signatures' '\n>  \t)\n>  '\n>  \n> +test_expect_success GPG 'verify-commit exits failure on unknown signature' '\n> +\ttest_must_fail git verify-commit thirteenth-signed 2>actual &&\n> +\t! grep \"Good signature from\" actual &&\n> +\t! grep \"BAD signature from\" actual &&\n> +\tgrep -q -F -e \"No public key\" -e \"public key not found\" actual\n> +'\n> +\n>  test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n>  \tgit verify-commit eighth-signed-alt 2>actual &&\n>  \tgrep \"Good signature from\" actual &&\n> @@ -338,6 +363,8 @@ test_expect_success GPG 'show double signature with custom format' '\n>  '\n>  \n>  \n> +# NEEDSWORK: This test relies on the test_tick commit/author dates from the first\n> +# 'create signed commits' test even though it creates its own\n>  test_expect_success GPG 'verify-commit verifies multiply signed commits' '\n>  \tgit init multiply-signed &&\n>  \tcd multiply-signed &&\n\nThe t7510-signed-commit.sh script hangs on startup with this change, and\nwith -vx we show:\n    \n    [...]\n    ++ git tag twelfth-signed-alt 17f06d503ee50df92746c17f6cced6feb5940cf5\n    ++ cat\n    ++ gpg --batch --gen-key keydetails\n    gpg: skipping control `%no-protection' ()\n\nThis is on a CentOS 7.9 box on the GCC Farm:\n    \n    [avar@gcc135 t]$ uname -a ; gpg --version\n    Linux gcc135.osuosl.org 4.18.0-80.7.2.el7.ppc64le #1 SMP Thu Sep 12 15:45:05 UTC 2019 ppc64le ppc64le ppc64le GNU/Linux\n    gpg (GnuPG) 2.0.22\n    libgcrypt 1.5.3\n    Copyright (C) 2013 Free Software Foundation, Inc.\n    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n    This is free software: you are free to change and redistribute it.\n    There is NO WARRANTY, to the extent permitted by law.\n    \n    Home: ~/.gnupg\n    Supported algorithms:\n    Pubkey: RSA, ?, ?, ELG, DSA\n    Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,\n            CAMELLIA128, CAMELLIA192, CAMELLIA256\n    Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224\n    Compression: Uncompressed, ZIP, ZLIB, BZIP2\n"},{"id":"444758","messageId":"20211222101326.fwl3wphr3ev6c7wt@fs","threadId":"56054","inReplyTo":"211222.86ilvhpbl0.gmgdl@evledraar.gmail.com","subject":"Re: t7510-signed-commit.sh hangs on old gpg, regression in 1bfb57f642d (was: [PATCH v8 9/9] ssh signing: test that gpg fails for unknown keys)","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-12-22T10:13:26Z","receivedAt":"2021-12-22T10:13:32Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 22.12.2021 04:18, Ævar Arnfjörð Bjarmason wrote:\n>\n>On Fri, Sep 10 2021, Fabian Stelzer via GitGitGadget wrote:\n>\n>> From: Fabian Stelzer <fs@gigacodes.de>\n>>\n>> Test that verify-commit/tag will fail when a gpg key is completely\n>> unknown. To do this we have to generate a key, use it for a signature\n>> and delete it from our keyring aferwards completely.\n>>\n>> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n>> +\n>> +\tcat >keydetails <<-\\EOF &&\n>> +\tKey-Type: RSA\n>> +\tKey-Length: 2048\n>> +\tSubkey-Type: RSA\n>> +\tSubkey-Length: 2048\n>> +\tName-Real: Unknown User\n>> +\tName-Email: unknown@git.com\n>> +\tExpire-Date: 0\n>> +\t%no-ask-passphrase\n>> +\t%no-protection\n>> +\tEOF\n>> +\tgpg --batch --gen-key keydetails &&\n>>\n>The t7510-signed-commit.sh script hangs on startup with this change, and\n>with -vx we show:\n>\n>    [...]\n>    ++ git tag twelfth-signed-alt 17f06d503ee50df92746c17f6cced6feb5940cf5\n>    ++ cat\n>    ++ gpg --batch --gen-key keydetails\n>    gpg: skipping control `%no-protection' ()\n>\n>This is on a CentOS 7.9 box on the GCC Farm:\n>\n>    [avar@gcc135 t]$ uname -a ; gpg --version\n>    Linux gcc135.osuosl.org 4.18.0-80.7.2.el7.ppc64le #1 SMP Thu Sep 12 15:45:05 UTC 2019 ppc64le ppc64le ppc64le GNU/Linux\n>    gpg (GnuPG) 2.0.22\n>    libgcrypt 1.5.3\n>    Copyright (C) 2013 Free Software Foundation, Inc.\n>    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n>    This is free software: you are free to change and redistribute it.\n>    There is NO WARRANTY, to the extent permitted by law.\n>\n>    Home: ~/.gnupg\n>    Supported algorithms:\n>    Pubkey: RSA, ?, ?, ELG, DSA\n>    Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,\n>            CAMELLIA128, CAMELLIA192, CAMELLIA256\n>    Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224\n>    Compression: Uncompressed, ZIP, ZLIB, BZIP2\n\nHm. I have an identical centos 7.9 installation (same versions/features) and \nthe key is generated without issues. Does the VM maybe have not enough \nentropy for generating a gpg key?\nOtherwise we could of course pre-generate the key and commit it. I'm usually \nnot a fan of this since over time it can become unclear how it was generated \nor if the committed version still matches what would be generated today.\nBut of course I don't want to slow down CI with rsa key generation stuff :/\nIf missing entropy is the problem, then maybe CI could benefit from \nsomething like haveged in general (other tests might want more entropy too).\n\n"},{"id":"444783","messageId":"YcNLOsuAh85ecKw4@camp.crustytoothpaste.net","threadId":"56054","inReplyTo":"20211222101326.fwl3wphr3ev6c7wt@fs","subject":"Re: t7510-signed-commit.sh hangs on old gpg, regression in 1bfb57f642d (was: [PATCH v8 9/9] ssh signing: test that gpg fails for unknown keys)","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2021-12-22T15:58:50Z","receivedAt":"2021-12-22T15:59:28Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2021-12-22 at 10:13:26, Fabian Stelzer wrote:\n> Hm. I have an identical centos 7.9 installation (same versions/features) and\n> the key is generated without issues. Does the VM maybe have not enough\n> entropy for generating a gpg key?\n> Otherwise we could of course pre-generate the key and commit it. I'm usually\n> not a fan of this since over time it can become unclear how it was generated\n> or if the committed version still matches what would be generated today.\n> But of course I don't want to slow down CI with rsa key generation stuff :/\n> If missing entropy is the problem, then maybe CI could benefit from\n> something like haveged in general (other tests might want more entropy too).\n\nGnuPG is notorious for using /dev/random for generating keys, so yes,\nthis is likely to block in a variety of situations.  We don't see this\non newer systems because they've replaced the blocking /dev/random with\na non-blocking one except for when the CSPRNG hasn't been seeded at\nleast once.\n\nThe problem isn't lack of entropy, but the fact that there's no reason\nto use /dev/random since /dev/urandom is suitable for all cryptographic\nneeds once initialized.  On modern versions of Linux, one just uses\ngetrandom(2), which deals with the uninitialized case and otherwise\ndoesn't block.  However, CentOS 7 is old.\n-- \nbrian m. carlson (he/him or they/them)\nToronto, Ontario, CA\n"},{"id":"444982","messageId":"211227.86h7avezrv.gmgdl@evledraar.gmail.com","threadId":"56054","inReplyTo":"20211222101326.fwl3wphr3ev6c7wt@fs","subject":"Re: t7510-signed-commit.sh hangs on old gpg, regression in 1bfb57f642d (was: [PATCH v8 9/9] ssh signing: test that gpg fails for unknown keys)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-12-26T22:53:47Z","receivedAt":"2021-12-26T23:02:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Dec 22 2021, Fabian Stelzer wrote:\n\n> On 22.12.2021 04:18, Ævar Arnfjörð Bjarmason wrote:\n>>\n>>On Fri, Sep 10 2021, Fabian Stelzer via GitGitGadget wrote:\n>>\n>>> From: Fabian Stelzer <fs@gigacodes.de>\n>>>\n>>> Test that verify-commit/tag will fail when a gpg key is completely\n>>> unknown. To do this we have to generate a key, use it for a signature\n>>> and delete it from our keyring aferwards completely.\n>>>\n>>> Signed-off-by: Fabian Stelzer <fs@gigacodes.de>\n>>> +\n>>> +\tcat >keydetails <<-\\EOF &&\n>>> +\tKey-Type: RSA\n>>> +\tKey-Length: 2048\n>>> +\tSubkey-Type: RSA\n>>> +\tSubkey-Length: 2048\n>>> +\tName-Real: Unknown User\n>>> +\tName-Email: unknown@git.com\n>>> +\tExpire-Date: 0\n>>> +\t%no-ask-passphrase\n>>> +\t%no-protection\n>>> +\tEOF\n>>> +\tgpg --batch --gen-key keydetails &&\n>>>\n>>The t7510-signed-commit.sh script hangs on startup with this change, and\n>>with -vx we show:\n>>\n>>    [...]\n>>    ++ git tag twelfth-signed-alt 17f06d503ee50df92746c17f6cced6feb5940cf5\n>>    ++ cat\n>>    ++ gpg --batch --gen-key keydetails\n>>    gpg: skipping control `%no-protection' ()\n>>\n>>This is on a CentOS 7.9 box on the GCC Farm:\n>>\n>>    [avar@gcc135 t]$ uname -a ; gpg --version\n>>    Linux gcc135.osuosl.org 4.18.0-80.7.2.el7.ppc64le #1 SMP Thu Sep 12 15:45:05 UTC 2019 ppc64le ppc64le ppc64le GNU/Linux\n>>    gpg (GnuPG) 2.0.22\n>>    libgcrypt 1.5.3\n>>    Copyright (C) 2013 Free Software Foundation, Inc.\n>>    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n>>    This is free software: you are free to change and redistribute it.\n>>    There is NO WARRANTY, to the extent permitted by law.\n>>\n>>    Home: ~/.gnupg\n>>    Supported algorithms:\n>>    Pubkey: RSA, ?, ?, ELG, DSA\n>>    Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,\n>>            CAMELLIA128, CAMELLIA192, CAMELLIA256\n>>    Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224\n>>    Compression: Uncompressed, ZIP, ZLIB, BZIP2\n>\n> Hm. I have an identical centos 7.9 installation (same\n> versions/features) and the key is generated without issues. Does the\n> VM maybe have not enough entropy for generating a gpg key?\n> Otherwise we could of course pre-generate the key and commit it. I'm\n> usually not a fan of this since over time it can become unclear how it\n> was generated or if the committed version still matches what would be\n> generated today.\n> But of course I don't want to slow down CI with rsa key generation stuff :/\n> If missing entropy is the problem, then maybe CI could benefit from\n> something like haveged in general (other tests might want more entropy\n> too).\n\nLate reply. It's not a VM, but yes. I've confirmed that it's due to\n/dev/random hanging.\n\nI don't understand why we need to generate a key at all.\n\nIt looks like your 1bfb57f642d (ssh signing: test that gpg fails for\nunknown keys, 2021-09-10) is just trying to test the case where we sign\nwith a key, and then don't have that key anymore.\n\nThe below POC patch seems to work just as well, and will succeed with:\n\n    ./t7510-signed-commit.sh --run=1,3\n\nOf course a lot of other tests now fail, because they relied on the\ndiscord@example.net key.\n\nBut that seems easily solved by just moving this test to its own file,\nor deleting/re-importing the key for just that test or whatever. If we\ntruly need yet another key why are we making it on the fly instead of\nadding it to t/lib-gpg/keyring.gpg like the others?\n\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 9882b69ae29..eec2a045cbc 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -73,23 +73,11 @@ test_expect_success GPG 'create signed commits' '\n \ttest_line_count = 1 oid &&\n \tgit tag twelfth-signed-alt $(cat oid) &&\n \n-\tcat >keydetails <<-\\EOF &&\n-\tKey-Type: RSA\n-\tKey-Length: 2048\n-\tSubkey-Type: RSA\n-\tSubkey-Length: 2048\n-\tName-Real: Unknown User\n-\tName-Email: unknown@git.com\n-\tExpire-Date: 0\n-\t%no-ask-passphrase\n-\t%no-protection\n-\tEOF\n-\tgpg --batch --gen-key keydetails &&\n-\techo 13 >file && git commit -a -S\"unknown@git.com\" -m thirteenth &&\n+\techo 13 >file && git commit -a -S\"discord@example.net\" -m thirteenth &&\n \tgit tag thirteenth-signed &&\n-\tDELETE_FINGERPRINT=$(gpg -K --with-colons --fingerprint --batch unknown@git.com | grep \"^fpr\" | head -n 1 | awk -F \":\" \"{print \\$10;}\") &&\n+\tDELETE_FINGERPRINT=$(gpg -K --with-colons --fingerprint --batch discord@example.net | grep \"^fpr\" | head -n 1 | awk -F \":\" \"{print \\$10;}\") &&\n \tgpg --batch --yes --delete-secret-keys $DELETE_FINGERPRINT &&\n-\tgpg --batch --yes --delete-keys unknown@git.com\n+\tgpg --batch --yes --delete-keys discord@example.net\n '\n \n test_expect_success GPG 'verify and show signatures' '\n"},{"id":"445217","messageId":"20211230111038.jtoqytdhkilv2732@fs","threadId":"56054","inReplyTo":"211227.86h7avezrv.gmgdl@evledraar.gmail.com","subject":"Re: t7510-signed-commit.sh hangs on old gpg, regression in 1bfb57f642d (was: [PATCH v8 9/9] ssh signing: test that gpg fails for unknown keys)","fromName":"Fabian Stelzer","fromEmail":"fs@gigacodes.de","sentAt":"2021-12-30T11:10:38Z","receivedAt":"2021-12-30T11:10:50Z","isPatch":true,"sender":{"key":"fs@gigacodes.de","avatar":"https://avatars.githubusercontent.com/u/564858?v=4"},"body":"On 26.12.2021 23:53, Ævar Arnfjörð Bjarmason wrote:\n>>\n>> Hm. I have an identical centos 7.9 installation (same\n>> versions/features) and the key is generated without issues. Does the\n>> VM maybe have not enough entropy for generating a gpg key?\n>> Otherwise we could of course pre-generate the key and commit it. I'm\n>> usually not a fan of this since over time it can become unclear how it\n>> was generated or if the committed version still matches what would be\n>> generated today.\n>> But of course I don't want to slow down CI with rsa key generation stuff :/\n>> If missing entropy is the problem, then maybe CI could benefit from\n>> something like haveged in general (other tests might want more entropy\n>> too).\n>\n>Late reply. It's not a VM, but yes. I've confirmed that it's due to\n>/dev/random hanging.\n>\n>I don't understand why we need to generate a key at all.\n\nYou are right, we don't need to. I initially toyed with the GPG commands to \ndisable/export/reimport a key but without success (I'm not terribly familiar \nwith GPG though). \n\n>\n>It looks like your 1bfb57f642d (ssh signing: test that gpg fails for\n>unknown keys, 2021-09-10) is just trying to test the case where we sign\n>with a key, and then don't have that key anymore.\n>\n\nIt tests verifying a commit for which the key is not in our keyring at all.  \nAll the other tests only use present keys (with varying trust levels) or \ncompletely unsigned commits for the failure check. \n\nI think we could do the following though and simply point git to an empty \nkeyring to be able to verify this:\n\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 9882b69ae2..2d38580847 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -71,25 +71,7 @@ test_expect_success GPG 'create signed commits' '\n  \tgit tag eleventh-signed $(cat oid) &&\n  \techo 12 | git commit-tree --gpg-sign=B7227189 HEAD^{tree} >oid &&\n  \ttest_line_count = 1 oid &&\n-\tgit tag twelfth-signed-alt $(cat oid) &&\n-\n-\tcat >keydetails <<-\\EOF &&\n-\tKey-Type: RSA\n-\tKey-Length: 2048\n-\tSubkey-Type: RSA\n-\tSubkey-Length: 2048\n-\tName-Real: Unknown User\n-\tName-Email: unknown@git.com\n-\tExpire-Date: 0\n-\t%no-ask-passphrase\n-\t%no-protection\n-\tEOF\n-\tgpg --batch --gen-key keydetails &&\n-\techo 13 >file && git commit -a -S\"unknown@git.com\" -m thirteenth &&\n-\tgit tag thirteenth-signed &&\n-\tDELETE_FINGERPRINT=$(gpg -K --with-colons --fingerprint --batch unknown@git.com | grep \"^fpr\" | head -n 1 | awk -F \":\" \"{print \\$10;}\") &&\n-\tgpg --batch --yes --delete-secret-keys $DELETE_FINGERPRINT &&\n-\tgpg --batch --yes --delete-keys unknown@git.com\n+\tgit tag twelfth-signed-alt $(cat oid)\n  '\n  \n  test_expect_success GPG 'verify and show signatures' '\n@@ -129,7 +111,7 @@ test_expect_success GPG 'verify and show signatures' '\n  '\n  \n  test_expect_success GPG 'verify-commit exits failure on unknown signature' '\n-\ttest_must_fail git verify-commit thirteenth-signed 2>actual &&\n+\tGNUPGHOME=./empty_home test_must_fail git verify-commit initial 2>actual &&\n  \t! grep \"Good signature from\" actual &&\n  \t! grep \"BAD signature from\" actual &&\n  \tgrep -q -F -e \"No public key\" -e \"public key not found\" actual\n\n\n"}]}