Message ID | 725764018ceb5bcecc748cc5169d4305ea9d7d23.1627501009.git.gitgitgadget@gmail.com (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen | expand |
"Fabian Stelzer via GitGitGadget" <gitgitgadget@gmail.com> writes: > From: Fabian Stelzer <fs@gigacodes.de> > > to verify a ssh signature we first call ssh-keygen -Y find-principal to "to" -> "To". > look up the signing principal by their public key from the > allowedSignersFile. If the key is found then we do a verify. Otherwise > we only validate the signature but can not verify the signers identity. > > Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED > SIGNERS") which contains valid public keys and a principal (usually > user@domain). Depending on the environment this file can be managed by > the individual developer or for example generated by the central > repository server from known ssh keys with push access. If the > repository only allows signed commits / pushes then the file can even be > stored inside it. > > To revoke a key put the public key without the principal prefix into > gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1) > "KEY REVOCATION LISTS"). The same considerations about who to trust for > verification as with the allowedSignersFile apply. > > Using SSH CA Keys with these files is also possible. Add > "cert-authority" as key option between the principal and the key to mark > it as a CA and all keys signed by it as valid for this CA. > > Signed-off-by: Fabian Stelzer <fs@gigacodes.de> > --- > builtin/receive-pack.c | 2 + > gpg-interface.c | 179 ++++++++++++++++++++++++++++++++++++++++- > 2 files changed, 180 insertions(+), 1 deletion(-) A lot of additions to support a new system, all looking quite straight-forward. > @@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = { > .program = "ssh-keygen", > .verify_args = ssh_verify_args, > .sigs = ssh_sigs, > - .verify_signed_buffer = NULL, /* TODO */ > + .verify_signed_buffer = verify_ssh_signed_buffer, > .sign_buffer = sign_buffer_ssh > }, > }; Nice. > @@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc, > return ret; > } > > +static void parse_ssh_output(struct signature_check *sigc) > +{ > + const char *line, *principal, *search; > + > + /* > + * ssh-keysign output should be: > + * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT > + * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT A bit unfortunate line that is overly long. These two are not mutually exclusive two different choices, but one is a special case of the other, no? How about phrasing it like so instead? /* * ssh-keysign output should be: * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT * * or for valid but unknown keys: * Good "git" signature with RSA key SHA256:FINGERPRINT * * Note that "PRINCIPAL" can contain whitespace, "RSA" and * "SHA256" part could be a different token that names of * the algorithms used, and "FINGERPRINT" is a hexadecimal * string. By finding the last occurence of " with ", we can * reliably parse out the PRINCIPAL. */ > + * or for valid but unknown keys: > + * Good "git" signature with RSA key SHA256:FINGERPRINT > + */ > + sigc->result = 'B'; > + sigc->trust_level = TRUST_NEVER; > + > + line = xmemdupz(sigc->output, strcspn(sigc->output, "\n")); > + > + if (skip_prefix(line, "Good \"git\" signature for ", &line)) { > + /* Valid signature and known principal */ > + sigc->result = 'G'; > + sigc->trust_level = TRUST_FULLY; > + > + /* Search for the last "with" to get the full principal */ > + principal = line; > + do { > + search = strstr(line, " with "); > + if (search) > + line = search + 1; > + } while (search != NULL); > + sigc->signer = xmemdupz(principal, line - principal - 1); > + sigc->fingerprint = xstrdup(strstr(line, "key") + 4); OK. This does not care the "RSA" part, which is future resistant. It assumes the <algo>:<fingerprint> comes after literal " key ", which I think is a reasonable thing to do. However, we never checked if the line has "key" in it, so strstr(line, "key") + 4 may not be pointing at where this code expects. > + sigc->key = xstrdup(sigc->fingerprint); > + } else if (skip_prefix(line, "Good \"git\" signature with ", &line)) { > + /* Valid signature, but key unknown */ > + sigc->result = 'G'; > + sigc->trust_level = TRUST_UNDEFINED; > + sigc->fingerprint = xstrdup(strstr(line, "key") + 4); > + sigc->key = xstrdup(sigc->fingerprint); Likewise, I guess. > + } > +} > + > +static int verify_ssh_signed_buffer(struct signature_check *sigc, > + struct gpg_format *fmt, const char *payload, > + size_t payload_size, const char *signature, > + size_t signature_size) > +{ > + struct child_process ssh_keygen = CHILD_PROCESS_INIT; > + struct tempfile *buffer_file; > + int ret = -1; > + const char *line; > + size_t trust_size; > + char *principal; > + struct strbuf ssh_keygen_out = STRBUF_INIT; > + struct strbuf ssh_keygen_err = STRBUF_INIT; > + > + if (!ssh_allowed_signers) { > + error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification")); > + return -1; > + } > + > + buffer_file = mks_tempfile_t(".git_vtag_tmpXXXXXX"); > + if (!buffer_file) > + return error_errno(_("could not create temporary file")); > + if (write_in_full(buffer_file->fd, signature, signature_size) < 0 || > + close_tempfile_gently(buffer_file) < 0) { > + error_errno(_("failed writing detached signature to '%s'"), > + buffer_file->filename.buf); > + delete_tempfile(&buffer_file); > + return -1; > + } > + > + /* Find the principal from the signers */ > + strvec_pushl(&ssh_keygen.args, fmt->program, > + "-Y", "find-principals", > + "-f", ssh_allowed_signers, > + "-s", buffer_file->filename.buf, > + NULL); > + ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, > + &ssh_keygen_err, 0); > + if (ret && strstr(ssh_keygen_err.buf, "usage:")) { > + error(_("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)")); > + goto out; > + } > + if (ret || !ssh_keygen_out.len) { > + /* We did not find a matching principal in the allowedSigners - Check > + * without validation */ > + child_process_init(&ssh_keygen); > + strvec_pushl(&ssh_keygen.args, fmt->program, > + "-Y", "check-novalidate", > + "-n", "git", > + "-s", buffer_file->filename.buf, > + NULL); > + ret = pipe_command(&ssh_keygen, payload, payload_size, > + &ssh_keygen_out, 0, &ssh_keygen_err, 0); > + } else { > + /* Check every principal we found (one per line) */ > + for (line = ssh_keygen_out.buf; *line; > + line = strchrnul(line + 1, '\n')) { > + while (*line == '\n') > + line++; > + if (!*line) > + break; > + > + trust_size = strcspn(line, "\n"); > + principal = xmemdupz(line, trust_size); > + > + child_process_init(&ssh_keygen); > + strbuf_release(&ssh_keygen_out); > + strbuf_release(&ssh_keygen_err); > + strvec_push(&ssh_keygen.args, fmt->program); > + /* We found principals - Try with each until we find a > + * match */ /* * Do not forget our multi-line comment * style, please. */ > + strvec_pushl(&ssh_keygen.args, "-Y", "verify", > + "-n", "git", > + "-f", ssh_allowed_signers, > + "-I", principal, > + "-s", buffer_file->filename.buf, > + NULL); > + > + if (ssh_revocation_file) { > + if (file_exists(ssh_revocation_file)) { > + strvec_pushl(&ssh_keygen.args, "-r", > + ssh_revocation_file, NULL); > + } else { > + warning(_("ssh signing revocation file configured but not found: %s"), > + ssh_revocation_file); > + } > + } > + > + sigchain_push(SIGPIPE, SIG_IGN); > + ret = pipe_command(&ssh_keygen, payload, payload_size, > + &ssh_keygen_out, 0, &ssh_keygen_err, 0); > + sigchain_pop(SIGPIPE); > + > + FREE_AND_NULL(principal); > + > + ret &= starts_with(ssh_keygen_out.buf, "Good"); This is somewhat unusual construct in our codebase, I suspect. And probably is even wrong. Didn't you mean if (!ret) ret = starts_with(...); instead? Surely, when pipe_command() failed, it is likely that ssh_keygen_out may not have anything useful, and checking what the first up-to-four bytes of it contain unconditionally may be cheap enough, but the person reading the code would expect you to peek into the result only when you actually got the result, no? > + if (ret == 0) > + break; It's more common to do if (!ret) break; in our codebase; in other words, we prefer not to compare with literal 0, like "if (x == 0)" or "if (y != 0)". Thanks.
> to verify a ssh signature we first call ssh-keygen -Y find-principal to > look up the signing principal by their public key from the > allowedSignersFile. If the key is found then we do a verify. Otherwise > we only validate the signature but can not verify the signers identity. Is this the same behavior as GPG signing in Git? > Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED > SIGNERS") which contains valid public keys and a principal (usually > user@domain). Depending on the environment this file can be managed by > the individual developer or for example generated by the central > repository server from known ssh keys with push access. If the > repository only allows signed commits / pushes then the file can even be > stored inside it. Storing the allowedSignersFile in the repo is technically possible even if the repository does not allow signed commits/pushes, right? I would reword the last sentence as "This file is usually stored outside the repository, but if the repository only allows signed commits/pushes, the user might choose to store it in the repository". > Using SSH CA Keys with these files is also possible. Add > "cert-authority" as key option between the principal and the key to mark > it as a CA and all keys signed by it as valid for this CA. Is this functionality provided by SSH? I don't see "cert-authority" anywhere in the diff below. Also, I notice that the tests are all provided at the end. I think that it would be better for the tests to be incrementally provided along with the commit that introduces the relevant functionality, so it is clearer to the reviewers how it is supposed to work (and also for us to observe test coverage). > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c > index a34742513ac..62b11c5f3a4 100644 > --- a/builtin/receive-pack.c > +++ b/builtin/receive-pack.c > @@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) > { > int status = parse_hide_refs_config(var, value, "receive"); > > + git_gpg_config(var, value, NULL); > + > if (status) > return status; Check the return value of git_gpg_config() to see if that config was processed by that function - if yes, we can return early. > +static void parse_ssh_output(struct signature_check *sigc) > +{ > + const char *line, *principal, *search; > + > + /* > + * ssh-keysign output should be: > + * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT > + * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT > + * or for valid but unknown keys: > + * Good "git" signature with RSA key SHA256:FINGERPRINT > + */ Is this "ssh-keysign" or "ssh-keygen" output? Also, is this output documented to be stable even across locales? > + sigc->result = 'B'; > + sigc->trust_level = TRUST_NEVER; A discussion of trust levels should also be in the commit message or user documentation. > + if (!strcmp(var, "gpg.ssh.revocationFile")) { The "F" in "revocationFile" has to be lowercase. (If tests were included, as I suggested above, it might have been easier to catch this.)
On 28.07.21 23:55, Junio C Hamano wrote: > "Fabian Stelzer via GitGitGadget" <gitgitgadget@gmail.com> writes: > >> From: Fabian Stelzer <fs@gigacodes.de> >> >> to verify a ssh signature we first call ssh-keygen -Y find-principal to > > "to" -> "To". fixed [snip] >> + /* >> + * ssh-keysign output should be: >> + * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT >> + * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT > > A bit unfortunate line that is overly long. These two are not > mutually exclusive two different choices, but one is a special case > of the other, no? How about phrasing it like so instead? > > /* > * ssh-keysign output should be: > * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT > * > * or for valid but unknown keys: > * Good "git" signature with RSA key SHA256:FINGERPRINT > * > * Note that "PRINCIPAL" can contain whitespace, "RSA" and > * "SHA256" part could be a different token that names of > * the algorithms used, and "FINGERPRINT" is a hexadecimal > * string. By finding the last occurence of " with ", we can > * reliably parse out the PRINCIPAL. > */ > Yes, it's a special case that makes it a bit harder to parse. I will change the comment like you suggested. That makes it clear. >> + /* Search for the last "with" to get the full principal */ >> + principal = line; >> + do { >> + search = strstr(line, " with "); >> + if (search) >> + line = search + 1; >> + } while (search != NULL); >> + sigc->signer = xmemdupz(principal, line - principal - 1); >> + sigc->fingerprint = xstrdup(strstr(line, "key") + 4); > > OK. This does not care the "RSA" part, which is future resistant. > It assumes the <algo>:<fingerprint> comes after literal " key ", > which I think is a reasonable thing to do. > > However, we never checked if the line has "key" in it, so > strstr(line, "key") + 4 may not be pointing at where this code > expects. > Hmm. What would i do if i don't find "key"? Still mark the signature as valid an just leave fingerprint & key empty? >> + /* We found principals - Try with each until we find a >> + * match */ > > /* > * Do not forget our multi-line comment > * style, please. > */ > fixed. clang-format wordwrapped those :/ >> + >> + ret &= starts_with(ssh_keygen_out.buf, "Good"); > > This is somewhat unusual construct in our codebase, I suspect. And > probably is even wrong. Didn't you mean > > if (!ret) > ret = starts_with(...); > > instead? Surely, when pipe_command() failed, it is likely that > ssh_keygen_out may not have anything useful, and checking what the > first up-to-four bytes of it contain unconditionally may be cheap > enough, but the person reading the code would expect you to peek > into the result only when you actually got the result, no? you are correct. we don't need to look at the output when the command fails. > >> + if (ret == 0) >> + break; > > It's more common to do > > if (!ret) > break; > changed. Thanks
On 29.07.21 01:04, Jonathan Tan wrote: >> to verify a ssh signature we first call ssh-keygen -Y find-principal to >> look up the signing principal by their public key from the >> allowedSignersFile. If the key is found then we do a verify. Otherwise >> we only validate the signature but can not verify the signers identity. > > Is this the same behavior as GPG signing in Git? Not quite. GPG requires every signers public key to be in the keyring. But even then, the "UNDEFINED" Trust level is enough to be valid for commits (but not for merges). For SSH i did set the unknown keys to UNDEFINED as well and they will show up as valid but not have a principal to identify them. This way a project can decide wether to accept unknown keys by setting the gpg.mintrustlevel. So the default behaviour is different. The alternative would be to treat unknown keys always as invalid. > >> Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED >> SIGNERS") which contains valid public keys and a principal (usually >> user@domain). Depending on the environment this file can be managed by >> the individual developer or for example generated by the central >> repository server from known ssh keys with push access. If the >> repository only allows signed commits / pushes then the file can even be >> stored inside it. > > Storing the allowedSignersFile in the repo is technically possible even > if the repository does not allow signed commits/pushes, right? I would > reword the last sentence as "This file is usually stored outside the > repository, but if the repository only allows signed commits/pushes, the > user might choose to store it in the repository". yes, thats correct. I have changed the wording. > >> Using SSH CA Keys with these files is also possible. Add >> "cert-authority" as key option between the principal and the key to mark >> it as a CA and all keys signed by it as valid for this CA. > > Is this functionality provided by SSH? I don't see "cert-authority" > anywhere in the diff below. I'll add "See "CERTIFICATES" in ssh-keygen(1)." It is a SSH feature that i just wanted to make people aware of. > > Also, I notice that the tests are all provided at the end. I think that > it would be better for the tests to be incrementally provided along with > the commit that introduces the relevant functionality, so it is clearer > to the reviewers how it is supposed to work (and also for us to observe > test coverage). The problem is that nearly all of the tests use both signing & verification of signatures. I could move the initial test that creates all the signed commits but probably not much else. >> + git_gpg_config(var, value, NULL); > > Check the return value of git_gpg_config() to see if that config was > processed by that function - if yes, we can return early. > fixed >> +static void parse_ssh_output(struct signature_check *sigc) >> +{ >> + const char *line, *principal, *search; >> + >> + /* >> + * ssh-keysign output should be: >> + * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT >> + * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT >> + * or for valid but unknown keys: >> + * Good "git" signature with RSA key SHA256:FINGERPRINT >> + */ > > Is this "ssh-keysign" or "ssh-keygen" output? ssh-keygen. ssh-keysign is only used for host keys. But the names can get a bit confusing sometimes. i changed it to ssh-keygen here. > > Also, is this output documented to be stable even across locales? Not really :/ (it currently is not locale specific) The documentation states to only check the commands exit code. Do we trust the exit code enough to rely on it for verification? If so then i can move the main result and only parse the text for the signer/fingerprint info thats used in log formats. This way only the logs would break in case the output changes. I added the output check since the gpg code did so as well: ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); > >> + sigc->result = 'B'; >> + sigc->trust_level = TRUST_NEVER; > > A discussion of trust levels should also be in the commit message or > user documentation. > >> + if (!strcmp(var, "gpg.ssh.revocationFile")) { > > The "F" in "revocationFile" has to be lowercase. (If tests were > included, as I suggested above, it might have been easier to catch > this.) > fixed Thanks
On 29.07.21 11:48, Fabian Stelzer wrote: > On 29.07.21 01:04, Jonathan Tan wrote: >>> to verify a ssh signature we first call ssh-keygen -Y find-principal to >>> look up the signing principal by their public key from the >>> allowedSignersFile. If the key is found then we do a verify. Otherwise >>> we only validate the signature but can not verify the signers identity. >> >> Is this the same behavior as GPG signing in Git? > > Not quite. GPG requires every signers public key to be in the keyring. > But even then, the "UNDEFINED" Trust level is enough to be valid for > commits (but not for merges). > For SSH i did set the unknown keys to UNDEFINED as well and they will > show up as valid but not have a principal to identify them. > This way a project can decide wether to accept unknown keys by setting > the gpg.mintrustlevel. So the default behaviour is different. > The alternative would be to treat unknown keys always as invalid. > I thought a bit more about this and my approach is indeed problematic especially when a repo has both gpg and ssh signatures. The trust level setting can then not behave differently for both. My intention of still showing valid but unknown signatures in the log as ok (but unknown) was to encourage users to always sign their work even if they are not (yet) trusted in the allowedSignersFile. I think the way forward should be to treat unknown singing keys as not verified like gpg does. If a ssh key is verified and in the allowedSignersFile i would still set its trust level to "FULLY".
Fabian Stelzer <fs@gigacodes.de> writes: >>> + /* Search for the last "with" to get the full principal */ >>> + principal = line; >>> + do { >>> + search = strstr(line, " with "); >>> + if (search) >>> + line = search + 1; >>> + } while (search != NULL); >>> + sigc->signer = xmemdupz(principal, line - principal - 1); >>> + sigc->fingerprint = xstrdup(strstr(line, "key") + 4); >> OK. This does not care the "RSA" part, which is future resistant. >> It assumes the <algo>:<fingerprint> comes after literal " key ", >> which I think is a reasonable thing to do. >> However, we never checked if the line has "key" in it, so >> strstr(line, "key") + 4 may not be pointing at where this code >> expects. > > Hmm. What would i do if i don't find "key"? Still mark the signature > as valid an just leave fingerprint & key empty? We didn't get a satisfactory response from the ssh-keygen we expect that tells us that the external tool successfully decided that the signature is good or bad. I would feel safer if we said we did not see a good signature in such a case.
Fabian Stelzer <fs@gigacodes.de> writes: > On 29.07.21 01:04, Jonathan Tan wrote: > >> Also, is this output documented to be stable even across locales? > Not really :/ (it currently is not locale specific) We probably want to defeat l10n of the message by spawning it in the C locale regardless. > The documentation states to only check the commands exit code. Do we > trust the exit code enough to rely on it for verification? 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 keychain, no? > If so then i can move the main result and only parse the text for the > signer/fingerprint info thats used in log formats. This way only the > logs would break in case the output changes. > > I added the output check since the gpg code did so as well: > ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); Does ssh-keygen have a mode similar to gpg's --status-fd feature where its output is geared more towards being stable and marchine parseable than being human friendly, by the way? Thanks.
On July 29, 2021 4:46 PM, Junio wrote: >Fabian Stelzer <fs@gigacodes.de> writes: > >> On 29.07.21 01:04, Jonathan Tan wrote: >> >>> Also, is this output documented to be stable even across locales? >> Not really :/ (it currently is not locale specific) > >We probably want to defeat l10n of the message by spawning it in the C locale regardless. > >> The documentation states to only check the commands exit code. Do we >> trust the exit code enough to rely on it for verification? > >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 keychain, no? Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >> If so then i can move the main result and only parse the text for the >> signer/fingerprint info thats used in log formats. This way only the >> logs would break in case the output changes. >> >> I added the output check since the gpg code did so as well: >> ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); > >Does ssh-keygen have a mode similar to gpg's --status-fd feature where its output is geared more towards being stable and marchine >parseable than being human friendly, by the way? 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 output - a particular annoyance I get daily.
On 29.07.21 23:01, Randall S. Becker wrote: > On July 29, 2021 4:46 PM, Junio wrote: >> Fabian Stelzer <fs@gigacodes.de> writes: >> >>> On 29.07.21 01:04, Jonathan Tan wrote: >>> >>>> Also, is this output documented to be stable even across locales? >>> Not really :/ (it currently is not locale specific) >> >> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >> >>> The documentation states to only check the commands exit code. Do we >>> trust the exit code enough to rely on it for verification? >> >> 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 > keychain, no? > > Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. > To find the principal (who signed it) we don't have to parse the output. Since verification is first a call to look up the principals matching the signatures public key from the allowedSignersFile and then trying verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH CA). Of course this even more relies on the exit code of ssh-keygen. Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. >>> If so then i can move the main result and only parse the text for the >>> signer/fingerprint info thats used in log formats. This way only the >>> logs would break in case the output changes. >>> >>> I added the output check since the gpg code did so as well: >>> ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); >> >> Does ssh-keygen have a mode similar to gpg's --status-fd feature where its output is geared more towards being stable and marchine >> parseable than being human friendly, by the way? > > 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 > output - a particular annoyance I get daily. >
On July 29, 2021 5:13 PM, Fabian Stelzer wrote: >Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures > >On 29.07.21 23:01, Randall S. Becker wrote: >> On July 29, 2021 4:46 PM, Junio wrote: >>> Fabian Stelzer <fs@gigacodes.de> writes: >>> >>>> On 29.07.21 01:04, Jonathan Tan wrote: >>>> >>>>> Also, is this output documented to be stable even across locales? >>>> Not really :/ (it currently is not locale specific) >>> >>> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >>> >>>> The documentation states to only check the commands exit code. Do we >>>> trust the exit code enough to rely on it for verification? >>> >>> 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 >> keychain, no? >> >> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >> > >To find the principal (who signed it) we don't have to parse the output. >Since verification is first a call to look up the principals matching the signatures public key from the allowedSignersFile and then trying >verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH >CA). >Of course this even more relies on the exit code of ssh-keygen. > >Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. What about a configurable exit code for this? See the comment below about that. >>>> If so then i can move the main result and only parse the text for >>>> the signer/fingerprint info thats used in log formats. This way only >>>> the logs would break in case the output changes. >>>> >>>> I added the output check since the gpg code did so as well: >>>> ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); >>> >>> Does ssh-keygen have a mode similar to gpg's --status-fd feature >>> where its output is geared more towards being stable and marchine parseable than being human friendly, by the way? >> >> 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 output - a particular annoyance I get daily. >> 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.
On 29.07.21 23:25, Randall S. Becker wrote: > On July 29, 2021 5:13 PM, Fabian Stelzer wrote: >> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures >> >> On 29.07.21 23:01, Randall S. Becker wrote: >>> On July 29, 2021 4:46 PM, Junio wrote: >>>> Fabian Stelzer <fs@gigacodes.de> writes: >>>> >>>>> On 29.07.21 01:04, Jonathan Tan wrote: >>>>> >>>>>> Also, is this output documented to be stable even across locales? >>>>> Not really :/ (it currently is not locale specific) >>>> >>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >>>> >>>>> The documentation states to only check the commands exit code. Do we >>>>> trust the exit code enough to rely on it for verification? >>>> >>>> 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 >>> keychain, no? >>> >>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >>> >> >> To find the principal (who signed it) we don't have to parse the output. >> Since verification is first a call to look up the principals matching the signatures public key from the allowedSignersFile and then trying >> verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH >> CA). >> Of course this even more relies on the exit code of ssh-keygen. >> >> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. > > What about a configurable exit code for this? See the comment below about that. > I'm not sure what you mean. Something like "treat exit(123) as success"? >>>>> If so then i can move the main result and only parse the text for >>>>> the signer/fingerprint info thats used in log formats. This way only >>>>> the logs would break in case the output changes. >>>>> >>>>> I added the output check since the gpg code did so as well: >>>>> ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); >>>> >>>> Does ssh-keygen have a mode similar to gpg's --status-fd feature >>>> where its output is geared more towards being stable and marchine parseable than being human friendly, by the way? >>> >>> 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 output - a particular annoyance I get daily. >>> > > 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. > This is already possible by setting gpg.ssh.program (although you'd have to pass the sign operation as well)
On July 29, 2021 5:29 PM, Fabian Stelzer wrote: >On 29.07.21 23:25, Randall S. Becker wrote: >> On July 29, 2021 5:13 PM, Fabian Stelzer wrote: >>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and >>> verify signatures >>> >>> On 29.07.21 23:01, Randall S. Becker wrote: >>>> On July 29, 2021 4:46 PM, Junio wrote: >>>>> Fabian Stelzer <fs@gigacodes.de> writes: >>>>> >>>>>> On 29.07.21 01:04, Jonathan Tan wrote: >>>>>> >>>>>>> Also, is this output documented to be stable even across locales? >>>>>> Not really :/ (it currently is not locale specific) >>>>> >>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >>>>> >>>>>> The documentation states to only check the commands exit code. Do >>>>>> we trust the exit code enough to rely on it for verification? >>>>> >>>>> 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 >>>> keychain, no? >>>> >>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >>>> >>> >>> To find the principal (who signed it) we don't have to parse the output. >>> Since verification is first a call to look up the principals matching >>> the signatures public key from the allowedSignersFile and then trying >>> verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH >CA). >>> Of course this even more relies on the exit code of ssh-keygen. >>> >>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. >> >> What about a configurable exit code for this? See the comment below about that. >> > >I'm not sure what you mean. Something like "treat exit(123) as success"? How about gpg.ssh.successExit=123 or something like that. >>>>>> If so then i can move the main result and only parse the text for >>>>>> the signer/fingerprint info thats used in log formats. This way >>>>>> only the logs would break in case the output changes. >>>>>> >>>>>> I added the output check since the gpg code did so as well: >>>>>> ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG "); >>>>> >>>>> Does ssh-keygen have a mode similar to gpg's --status-fd feature >>>>> where its output is geared more towards being stable and marchine parseable than being human friendly, by the way? >>>> >>>> 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 output - a particular annoyance I get daily. >>>> >> >> 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. >> > >This is already possible by setting gpg.ssh.program (although you'd have to pass the sign operation as well) 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. Thanks, Randall
On 30.07.21 00:28, Randall S. Becker wrote: > On July 29, 2021 5:29 PM, Fabian Stelzer wrote: >> On 29.07.21 23:25, Randall S. Becker wrote: >>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote: >>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and >>>> verify signatures >>>> >>>> On 29.07.21 23:01, Randall S. Becker wrote: >>>>> On July 29, 2021 4:46 PM, Junio wrote: >>>>>> Fabian Stelzer <fs@gigacodes.de> writes: >>>>>> >>>>>>> On 29.07.21 01:04, Jonathan Tan wrote: >>>>>>> >>>>>>>> Also, is this output documented to be stable even across locales? >>>>>>> Not really :/ (it currently is not locale specific) >>>>>> >>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >>>>>> >>>>>>> The documentation states to only check the commands exit code. Do >>>>>>> we trust the exit code enough to rely on it for verification? >>>>>> >>>>>> 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 >>>>> keychain, no? >>>>> >>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >>>>> >>>> >>>> To find the principal (who signed it) we don't have to parse the output. >>>> Since verification is first a call to look up the principals matching >>>> the signatures public key from the allowedSignersFile and then trying >>>> verification with each one we already know which one matched (usually there is only one. I think multiples is only possible with an SSH >> CA). >>>> Of course this even more relies on the exit code of ssh-keygen. >>>> >>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. >>> >>> What about a configurable exit code for this? See the comment below about that. >>> >> >> I'm not sure what you mean. Something like "treat exit(123) as success"? > > How about gpg.ssh.successExit=123 or something like that. > I don't quite understand what the benefit would be. Do you have any specific portability problems/concerns where the ssh-keygen format is different or exit codes differ? 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 already cover edge cases when needed. > > 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. > The documentation in ssh-keygen(1) is quite good and straight forward for verification and signing. Again if you have any specific portability concerns i'd be glad to help.
On July 30, 2021 4:17 AM, Fabian Stelzer wrote: >Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures > >On 30.07.21 00:28, Randall S. Becker wrote: >> On July 29, 2021 5:29 PM, Fabian Stelzer wrote: >>> On 29.07.21 23:25, Randall S. Becker wrote: >>>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote: >>>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output >>>>> and verify signatures >>>>> >>>>> On 29.07.21 23:01, Randall S. Becker wrote: >>>>>> On July 29, 2021 4:46 PM, Junio wrote: >>>>>>> Fabian Stelzer <fs@gigacodes.de> writes: >>>>>>> >>>>>>>> On 29.07.21 01:04, Jonathan Tan wrote: >>>>>>>> >>>>>>>>> Also, is this output documented to be stable even across locales? >>>>>>>> Not really :/ (it currently is not locale specific) >>>>>>> >>>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >>>>>>> >>>>>>>> The documentation states to only check the commands exit code. >>>>>>>> Do we trust the exit code enough to rely on it for verification? >>>>>>> >>>>>>> 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 >>>>>> keychain, no? >>>>>> >>>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >>>>>> >>>>> >>>>> To find the principal (who signed it) we don't have to parse the output. >>>>> Since verification is first a call to look up the principals >>>>> matching the signatures public key from the allowedSignersFile and >>>>> then trying verification with each one we already know which one >>>>> matched (usually there is only one. I think multiples is only >>>>> possible with an SSH >>> CA). >>>>> Of course this even more relies on the exit code of ssh-keygen. >>>>> >>>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. >>>> >>>> What about a configurable exit code for this? See the comment below about that. >>>> >>> >>> I'm not sure what you mean. Something like "treat exit(123) as success"? >> >> How about gpg.ssh.successExit=123 or something like that. >> > >I don't quite understand what the benefit would be. Do you have any specific portability problems/concerns where the ssh-keygen format >is different or exit codes differ? >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 >already cover edge cases when needed. > >> >> 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. >> > >The documentation in ssh-keygen(1) is quite good and straight forward for verification and signing. Again if you have any specific >portability concerns i'd be glad to help. I do know the ssh-keygen interface and that does not really answer my doubts. 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.
On 30.07.21 16:26, Randall S. Becker wrote: > On July 30, 2021 4:17 AM, Fabian Stelzer wrote: >> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures >> >> On 30.07.21 00:28, Randall S. Becker wrote: >>> On July 29, 2021 5:29 PM, Fabian Stelzer wrote: >>>> On 29.07.21 23:25, Randall S. Becker wrote: >>>>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote: >>>>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output >>>>>> and verify signatures >>>>>> >>>>>> On 29.07.21 23:01, Randall S. Becker wrote: >>>>>>> On July 29, 2021 4:46 PM, Junio wrote: >>>>>>>> Fabian Stelzer <fs@gigacodes.de> writes: >>>>>>>> >>>>>>>>> On 29.07.21 01:04, Jonathan Tan wrote: >>>>>>>>> >>>>>>>>>> Also, is this output documented to be stable even across locales? >>>>>>>>> Not really :/ (it currently is not locale specific) >>>>>>>> >>>>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >>>>>>>> >>>>>>>>> The documentation states to only check the commands exit code. >>>>>>>>> Do we trust the exit code enough to rely on it for verification? >>>>>>>> >>>>>>>> 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 >>>>>>> keychain, no? >>>>>>> >>>>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >>>>>>> >>>>>> >>>>>> To find the principal (who signed it) we don't have to parse the output. >>>>>> Since verification is first a call to look up the principals >>>>>> matching the signatures public key from the allowedSignersFile and >>>>>> then trying verification with each one we already know which one >>>>>> matched (usually there is only one. I think multiples is only >>>>>> possible with an SSH >>>> CA). >>>>>> Of course this even more relies on the exit code of ssh-keygen. >>>>>> >>>>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. >>>>> >>>>> What about a configurable exit code for this? See the comment below about that. >>>>> >>>> >>>> I'm not sure what you mean. Something like "treat exit(123) as success"? >>> >>> How about gpg.ssh.successExit=123 or something like that. >>> >> >> I don't quite understand what the benefit would be. Do you have any specific portability problems/concerns where the ssh-keygen format >> is different or exit codes differ? >> 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 >> already cover edge cases when needed. >> >>> >>> 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. >>> >> >> The documentation in ssh-keygen(1) is quite good and straight forward for verification and signing. Again if you have any specific >> portability concerns i'd be glad to help. > > I do know the ssh-keygen interface and that does not really answer my doubts. > > 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. > > Fair enough. Where would you expect to look for such documentation? I'm not sure sth like config/gpg.txt is the right place for this.
On July 30, 2021 10:32 AM, Fabian Stelzer wrote: >On 30.07.21 16:26, Randall S. Becker wrote: >> On July 30, 2021 4:17 AM, Fabian Stelzer wrote: >>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and >>> verify signatures >>> >>> On 30.07.21 00:28, Randall S. Becker wrote: >>>> On July 29, 2021 5:29 PM, Fabian Stelzer wrote: >>>>> On 29.07.21 23:25, Randall S. Becker wrote: >>>>>> On July 29, 2021 5:13 PM, Fabian Stelzer wrote: >>>>>>> Subject: Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output >>>>>>> and verify signatures >>>>>>> >>>>>>> On 29.07.21 23:01, Randall S. Becker wrote: >>>>>>>> On July 29, 2021 4:46 PM, Junio wrote: >>>>>>>>> Fabian Stelzer <fs@gigacodes.de> writes: >>>>>>>>> >>>>>>>>>> On 29.07.21 01:04, Jonathan Tan wrote: >>>>>>>>>> >>>>>>>>>>> Also, is this output documented to be stable even across locales? >>>>>>>>>> Not really :/ (it currently is not locale specific) >>>>>>>>> >>>>>>>>> We probably want to defeat l10n of the message by spawning it in the C locale regardless. >>>>>>>>> >>>>>>>>>> The documentation states to only check the commands exit code. >>>>>>>>>> Do we trust the exit code enough to rely on it for verification? >>>>>>>>> >>>>>>>>> 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 >>>>>>>> keychain, no? >>>>>>>> >>>>>>>> Have we not had issues in the past depending on exit code? I'm not sure this can be made entirely portable. >>>>>>>> >>>>>>> >>>>>>> To find the principal (who signed it) we don't have to parse the output. >>>>>>> Since verification is first a call to look up the principals >>>>>>> matching the signatures public key from the allowedSignersFile >>>>>>> and then trying verification with each one we already know which >>>>>>> one matched (usually there is only one. I think multiples is only >>>>>>> possible with an SSH >>>>> CA). >>>>>>> Of course this even more relies on the exit code of ssh-keygen. >>>>>>> >>>>>>> Not sure which is more portable and reliable. Parsing the textual output or the exit code. At the moment my patch does both. >>>>>> >>>>>> What about a configurable exit code for this? See the comment below about that. >>>>>> >>>>> >>>>> I'm not sure what you mean. Something like "treat exit(123) as success"? >>>> >>>> How about gpg.ssh.successExit=123 or something like that. >>>> >>> >>> I don't quite understand what the benefit would be. Do you have any >>> specific portability problems/concerns where the ssh-keygen format is different or exit codes differ? >>> 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 already cover edge cases when needed. >>> >>>> >>>> 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. >>>> >>> >>> The documentation in ssh-keygen(1) is quite good and straight forward >>> for verification and signing. Again if you have any specific portability concerns i'd be glad to help. >> >> I do know the ssh-keygen interface and that does not really answer my doubts. >> >> 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. >> >> > >Fair enough. Where would you expect to look for such documentation? >I'm not sure sth like config/gpg.txt is the right place for this. My 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. With my thanks, Randall
On 29.07.21 15:52, Fabian Stelzer wrote: > On 29.07.21 11:48, Fabian Stelzer wrote: >> On 29.07.21 01:04, Jonathan Tan wrote: >>>> to verify a ssh signature we first call ssh-keygen -Y find-principal to >>>> look up the signing principal by their public key from the >>>> allowedSignersFile. If the key is found then we do a verify. Otherwise >>>> we only validate the signature but can not verify the signers identity. >>> >>> Is this the same behavior as GPG signing in Git? >> >> Not quite. GPG requires every signers public key to be in the keyring. >> But even then, the "UNDEFINED" Trust level is enough to be valid for >> commits (but not for merges). >> For SSH i did set the unknown keys to UNDEFINED as well and they will >> show up as valid but not have a principal to identify them. >> This way a project can decide wether to accept unknown keys by setting >> the gpg.mintrustlevel. So the default behaviour is different. >> The alternative would be to treat unknown keys always as invalid. >> > > I thought a bit more about this and my approach is indeed problematic > especially when a repo has both gpg and ssh signatures. The trust level > setting can then not behave differently for both. > > My intention of still showing valid but unknown signatures in the log as > ok (but unknown) was to encourage users to always sign their work even > if they are not (yet) trusted in the allowedSignersFile. > > I think the way forward should be to treat unknown singing keys as not > verified like gpg does. > > If a ssh key is verified and in the allowedSignersFile i would still set > its trust level to "FULLY". i dug a bit deeper into the gpg code/tests and it actually already behaves the same. untrusted signatures still return successfull on a verify-commit/tag even if the key is completely untrusted. my patch does the same thing for ssh signatures. i'll send a new revision later today with all the other fixes.
On 03.08.21 09:43, Fabian Stelzer wrote: > > > On 29.07.21 15:52, Fabian Stelzer wrote: >> On 29.07.21 11:48, Fabian Stelzer wrote: >>> On 29.07.21 01:04, Jonathan Tan wrote: >>>>> to verify a ssh signature we first call ssh-keygen -Y >>>>> find-principal to >>>>> look up the signing principal by their public key from the >>>>> allowedSignersFile. If the key is found then we do a verify. Otherwise >>>>> we only validate the signature but can not verify the signers >>>>> identity. >>>> >>>> Is this the same behavior as GPG signing in Git? >>> >>> Not quite. GPG requires every signers public key to be in the >>> keyring. But even then, the "UNDEFINED" Trust level is enough to be >>> valid for commits (but not for merges). >>> For SSH i did set the unknown keys to UNDEFINED as well and they will >>> show up as valid but not have a principal to identify them. >>> This way a project can decide wether to accept unknown keys by >>> setting the gpg.mintrustlevel. So the default behaviour is different. >>> The alternative would be to treat unknown keys always as invalid. >>> >> >> I thought a bit more about this and my approach is indeed problematic >> especially when a repo has both gpg and ssh signatures. The trust >> level setting can then not behave differently for both. >> >> My intention of still showing valid but unknown signatures in the log >> as ok (but unknown) was to encourage users to always sign their work >> even if they are not (yet) trusted in the allowedSignersFile. >> >> I think the way forward should be to treat unknown singing keys as not >> verified like gpg does. >> >> If a ssh key is verified and in the allowedSignersFile i would still >> set its trust level to "FULLY". > > i dug a bit deeper into the gpg code/tests and it actually already > behaves the same. untrusted signatures still return successfull on a > verify-commit/tag even if the key is completely untrusted. my patch does > the same thing for ssh signatures. i'll send a new revision later today > with all the other fixes. oh boy... sorry for all the emails. the gpg stuff can be really confusing. especially since there's different meanings of "untrusted", "unknown" and "undefined" depending on which docs/codebase you look into. Especially "untrusted" is not really a gpg term but used in the codebase in tests like 'verify-commit exits success on untrusted signature' which tests for a key already in the keyring but not with any specified trust level. I could not actually find any gpg test for a signature that is completely unknown. (i will add one) GPG does a successful verify-commit/tag on keys that are "known". Meaning that to be marked as good signatures all you need is to have the public key in your keyring. This key can still have an unknown/undefined trust level (meaning its in the keyring but no decision on trust has been made). A key thats not in the keyring has no trustlevel or anything but fails hard with "no public key". SSH signing does not really make this distinction. A key is either in the allowedSigners file (and therefore trusted), completely unknown, or revoked via the revokedSigners file. To make this behave like gpg does i will make verification fail on completely unknown keys. There is no use of the undefined trust level for ssh then and i will set keys in the allowedSigners file to fully trusted so they will be accepted for merges as well. I don't see any way to have keys that are valid for commits but not merge with ssh then but that should be the only difference to gpg.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index a34742513ac..62b11c5f3a4 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) { int status = parse_hide_refs_config(var, value, "receive"); + git_gpg_config(var, value, NULL); + if (status) return status; diff --git a/gpg-interface.c b/gpg-interface.c index ec48a37b6cc..703225c3cd3 100644 --- a/gpg-interface.c +++ b/gpg-interface.c @@ -3,11 +3,13 @@ #include "config.h" #include "run-command.h" #include "strbuf.h" +#include "dir.h" #include "gpg-interface.h" #include "sigchain.h" #include "tempfile.h" static char *configured_signing_key; +static const char *ssh_allowed_signers, *ssh_revocation_file; static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED; struct gpg_format { @@ -51,6 +53,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc, struct gpg_format *fmt, const char *payload, size_t payload_size, const char *signature, size_t signature_size); +static int verify_ssh_signed_buffer(struct signature_check *sigc, + struct gpg_format *fmt, const char *payload, + size_t payload_size, const char *signature, + size_t signature_size); static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, const char *signing_key); static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature, @@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = { .program = "ssh-keygen", .verify_args = ssh_verify_args, .sigs = ssh_sigs, - .verify_signed_buffer = NULL, /* TODO */ + .verify_signed_buffer = verify_ssh_signed_buffer, .sign_buffer = sign_buffer_ssh }, }; @@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc, return ret; } +static void parse_ssh_output(struct signature_check *sigc) +{ + const char *line, *principal, *search; + + /* + * ssh-keysign output should be: + * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT + * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT + * or for valid but unknown keys: + * Good "git" signature with RSA key SHA256:FINGERPRINT + */ + sigc->result = 'B'; + sigc->trust_level = TRUST_NEVER; + + line = xmemdupz(sigc->output, strcspn(sigc->output, "\n")); + + if (skip_prefix(line, "Good \"git\" signature for ", &line)) { + /* Valid signature and known principal */ + sigc->result = 'G'; + sigc->trust_level = TRUST_FULLY; + + /* Search for the last "with" to get the full principal */ + principal = line; + do { + search = strstr(line, " with "); + if (search) + line = search + 1; + } while (search != NULL); + sigc->signer = xmemdupz(principal, line - principal - 1); + sigc->fingerprint = xstrdup(strstr(line, "key") + 4); + sigc->key = xstrdup(sigc->fingerprint); + } else if (skip_prefix(line, "Good \"git\" signature with ", &line)) { + /* Valid signature, but key unknown */ + sigc->result = 'G'; + sigc->trust_level = TRUST_UNDEFINED; + sigc->fingerprint = xstrdup(strstr(line, "key") + 4); + sigc->key = xstrdup(sigc->fingerprint); + } +} + +static int verify_ssh_signed_buffer(struct signature_check *sigc, + struct gpg_format *fmt, const char *payload, + size_t payload_size, const char *signature, + size_t signature_size) +{ + struct child_process ssh_keygen = CHILD_PROCESS_INIT; + struct tempfile *buffer_file; + int ret = -1; + const char *line; + size_t trust_size; + char *principal; + struct strbuf ssh_keygen_out = STRBUF_INIT; + struct strbuf ssh_keygen_err = STRBUF_INIT; + + if (!ssh_allowed_signers) { + error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification")); + return -1; + } + + buffer_file = mks_tempfile_t(".git_vtag_tmpXXXXXX"); + if (!buffer_file) + return error_errno(_("could not create temporary file")); + if (write_in_full(buffer_file->fd, signature, signature_size) < 0 || + close_tempfile_gently(buffer_file) < 0) { + error_errno(_("failed writing detached signature to '%s'"), + buffer_file->filename.buf); + delete_tempfile(&buffer_file); + return -1; + } + + /* Find the principal from the signers */ + strvec_pushl(&ssh_keygen.args, fmt->program, + "-Y", "find-principals", + "-f", ssh_allowed_signers, + "-s", buffer_file->filename.buf, + NULL); + ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, + &ssh_keygen_err, 0); + if (ret && strstr(ssh_keygen_err.buf, "usage:")) { + error(_("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)")); + goto out; + } + if (ret || !ssh_keygen_out.len) { + /* We did not find a matching principal in the allowedSigners - Check + * without validation */ + child_process_init(&ssh_keygen); + strvec_pushl(&ssh_keygen.args, fmt->program, + "-Y", "check-novalidate", + "-n", "git", + "-s", buffer_file->filename.buf, + NULL); + ret = pipe_command(&ssh_keygen, payload, payload_size, + &ssh_keygen_out, 0, &ssh_keygen_err, 0); + } else { + /* Check every principal we found (one per line) */ + for (line = ssh_keygen_out.buf; *line; + line = strchrnul(line + 1, '\n')) { + while (*line == '\n') + line++; + if (!*line) + break; + + trust_size = strcspn(line, "\n"); + principal = xmemdupz(line, trust_size); + + child_process_init(&ssh_keygen); + strbuf_release(&ssh_keygen_out); + strbuf_release(&ssh_keygen_err); + strvec_push(&ssh_keygen.args, fmt->program); + /* We found principals - Try with each until we find a + * match */ + strvec_pushl(&ssh_keygen.args, "-Y", "verify", + "-n", "git", + "-f", ssh_allowed_signers, + "-I", principal, + "-s", buffer_file->filename.buf, + NULL); + + if (ssh_revocation_file) { + if (file_exists(ssh_revocation_file)) { + strvec_pushl(&ssh_keygen.args, "-r", + ssh_revocation_file, NULL); + } else { + warning(_("ssh signing revocation file configured but not found: %s"), + ssh_revocation_file); + } + } + + sigchain_push(SIGPIPE, SIG_IGN); + ret = pipe_command(&ssh_keygen, payload, payload_size, + &ssh_keygen_out, 0, &ssh_keygen_err, 0); + sigchain_pop(SIGPIPE); + + FREE_AND_NULL(principal); + + ret &= starts_with(ssh_keygen_out.buf, "Good"); + if (ret == 0) + break; + } + } + + sigc->payload = xmemdupz(payload, payload_size); + strbuf_stripspace(&ssh_keygen_out, 0); + strbuf_stripspace(&ssh_keygen_err, 0); + strbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len); + sigc->output = strbuf_detach(&ssh_keygen_out, NULL); + sigc->gpg_status = xstrdup(sigc->output); + + parse_ssh_output(sigc); + +out: + if (buffer_file) + delete_tempfile(&buffer_file); + strbuf_release(&ssh_keygen_out); + strbuf_release(&ssh_keygen_err); + + return ret; +} + int check_signature(const char *payload, size_t plen, const char *signature, size_t slen, struct signature_check *sigc) { @@ -453,6 +618,18 @@ int git_gpg_config(const char *var, const char *value, void *cb) return 0; } + if (!strcmp(var, "gpg.ssh.allowedsignersfile")) { + if (!value) + return config_error_nonbool(var); + return git_config_string(&ssh_allowed_signers, var, value); + } + + if (!strcmp(var, "gpg.ssh.revocationFile")) { + if (!value) + return config_error_nonbool(var); + return git_config_string(&ssh_revocation_file, var, value); + } + if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program")) fmtname = "openpgp";