From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-06 08:19:59
From: Fabian Stelzer <redacted>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.
Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
needs openssh>8.2p1
Signed-off-by: Fabian Stelzer <redacted>
---
RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
Support for using private keyfiles directly is still missing and i'm
unsure on how to configure it or if the pubkey in the signingkey field
is such a good idea. A SSH Fingerprint as signingkey would be nicer, but
key lookup would be quite cumbersome. Maybe storing the fingerprint in
signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile
setting? As a default we could get the first ssh key from ssh-add and
store it in the config to avoid unintentional changes of the used
signing key. I've started with some tests for SSH Signing but having
static private keyfiles would make this a lot easier. So still on my
TODO.
This feature makes git signing much more accessible to the average user.
Usually they have a SSH Key for pushing code already. Using it for
signing commits allows us to verify not only the transport but the
pushed code as well. The allowed_signers file could be kept in the
repository if all receives are verified (allowing only useris with valid
signatures to add/change them) or outside if generated/managed
differently. Tools like gitolite could optionally generate and enforce
them from the already existing user ssh keys for example.
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which i think is quite common (at least
for the email part). This way we can establish the correct trust for the
SSH Keys without setting up a separate GPG Infrastructure (which is
still quite painful for users) or implementing x509 signing support for
git (which lacks good forwarding mechanisms). Using ssh agent forwarding
makes this feature easily usable in todays development environments
where code is often checked out in remote VMs / containers.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v1
Pull-Request: https://github.com/git/git/pull/1041
Documentation/config/gpg.txt | 13 ++-
Documentation/config/user.txt | 4 +
gpg-interface.c | 212 ++++++++++++++++++++++++++++++----
gpg-interface.h | 3 +
4 files changed, 205 insertions(+), 27 deletions(-)
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -27,6 +27,15 @@ gpg.minTrustLevel:: with at least `undefined` trust. Setting this option overrides the required trust-level for all operations. Supported values, in increasing order of significance:++gpg.ssh.allowedSigners::+ A file containing all valid SSH signing principals. + Similar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.+ Defaults to .gitsigners++gpg.ssh.revocationFile::+ Either a SSH KRL or a list of revoked public keys.+ See ssh-keygen(1) for details. + * `undefined` * `never`
@@ -36,3 +36,7 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports.+ If gpg.format is set to "ssh" this needs to contain the valid+ ssh public key (e.g.: "ssh-rsa XXXXXX identifier") which corresponds+ to the private key used for signing. The private key needs to be available+ via ssh-agent. Direct private key files are not supported yet.
@@ -144,6 +156,38 @@ static int parse_gpg_trust_level(const char *level,return1;}+staticvoidparse_ssh_output(structsignature_check*sigc)+{+constchar*output=NULL;+char*next=NULL;++// ssh-keysign output should be:+// Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT++output=xmemdupz(sigc->gpg_status,strcspn(sigc->gpg_status," \n"));+if(skip_prefix(sigc->gpg_status,"Good \"git\" signature for ",&output)){+sigc->result='G';++next=strchrnul(output,' ');+replace_cstring(&sigc->signer,output,next);+output=next+1;+next=strchrnul(output,' ');// 'with'+output=next+1;+next=strchrnul(output,' ');// KEY Type+output=next+1;+next=strchrnul(output,' ');// 'key'+output=next+1;+next=strchrnul(output,' ');// key+replace_cstring(&sigc->fingerprint,output,next);+}else{+sigc->result='B';+}++// SSH-Keygen prints onto stdout instead of stderr like the output code expects - so we just copy it over+free(sigc->gpg_output);+sigc->gpg_output=xmemdupz(sigc->gpg_status,strlen(sigc->gpg_status));+}+staticvoidparse_gpg_output(structsignature_check*sigc){constchar*buf=sigc->gpg_status;
@@ -283,24 +333,77 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,if(!fmt)BUG("bad signature '%s'",signature);-strvec_push(&gpg.args,fmt->program);-strvec_pushv(&gpg.args,fmt->verify_args);-strvec_pushl(&gpg.args,-"--status-fd=1",-"--verify",temp->filename.buf,"-",-NULL);+if(!strcmp(use_format->name,"ssh")){+// Find the principal from the signers+strvec_push(&ssh_keygen.args,fmt->program);+strvec_pushl(&ssh_keygen.args,"-Y","find-principals",+"-f",get_ssh_allowed_signers(),+"-s",temp->filename.buf,+NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&principal_out,0,&principal_err,0);+if(strstr(principal_err.buf,"unknown option")){+error(_("openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)"));+}+if(ret||!principal_out.len)+gotoout;++/* Iterate over all lines */+for(line=principal_out.buf;*line;line=strchrnul(line+1,'\n')){+while(*line=='\n')+line++;+if(!*line)+break;-if(!gpg_status)-gpg_status=&buf;+trust_size=strcspn(line," \n");+principal=xmemdupz(line,trust_size);-sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,payload,payload_size,-gpg_status,0,gpg_output,0);-sigchain_pop(SIGPIPE);+strvec_push(&gpg.args,fmt->program);+// We found principals - Try with each until we find a match+strvec_pushl(&gpg.args,"-Y","verify",+"-n","git",+"-f",get_ssh_allowed_signers(),+"-I",principal,+"-s",temp->filename.buf,+NULL);-delete_tempfile(&temp);+if(ssh_revocation_file){+strvec_pushl(&gpg.args,"-r",ssh_revocation_file,NULL);+}++if(!gpg_status)+gpg_status=&buf;++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&gpg,payload,payload_size,+gpg_status,0,gpg_output,0);+sigchain_pop(SIGPIPE);-ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");+ret|=!strstr(gpg_status->buf,"Good");+if(ret==0)+break;+}+}else{+strvec_push(&gpg.args,fmt->program);+strvec_pushv(&gpg.args,fmt->verify_args);+strvec_pushl(&gpg.args,+"--status-fd=1",+"--verify",temp->filename.buf,"-",+NULL);++if(!gpg_status)+gpg_status=&buf;++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&gpg,payload,payload_size,gpg_status,0,+gpg_output,0);+sigchain_pop(SIGPIPE);+ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");+}++out:+delete_tempfile(&temp);+strbuf_release(&principal_out);+strbuf_release(&principal_err);strbuf_release(&buf);/* no matter it was used or not */returnret;
@@ -437,7 +555,19 @@ const char *get_signing_key(void){if(configured_signing_key)returnconfigured_signing_key;-returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+if(!strcmp(use_format->name,"ssh")){+// We could simply use the first key listed by ssh-add -L and risk signing with the wrong key+return"";+}else{+returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+}+}++constchar*get_ssh_allowed_signers(void)+{+if(ssh_allowed_signers)+returnssh_allowed_signers;+returnGPG_SSH_ALLOWED_SIGNERS;}intsign_buffer(structstrbuf*buffer,structstrbuf*signature,constchar*signing_key)
@@ -446,12 +576,35 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigintret;size_ti,j,bottom;structstrbufgpg_status=STRBUF_INIT;--strvec_pushl(&gpg.args,-use_format->program,-"--status-fd=2",-"-bsau",signing_key,-NULL);+structtempfile*temp=NULL;++if(!strcmp(use_format->name,"ssh")){+if(!signing_key)+returnerror(_("user.signingkey needs to be set to a ssh public key for ssh signing"));++// signing_key is a public ssh key+// FIXME: Allow specifying a key file so we can use private keyfiles instead of ssh-agent+temp=mks_tempfile_t(".git_signing_key_tmpXXXXXX");+if(!temp)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(temp->fd,signing_key,+strlen(signing_key))<0||+close_tempfile_gently(temp)<0){+error_errno(_("failed writing ssh signing key to '%s'"),temp->filename.buf);+delete_tempfile(&temp);+return-1;+}+strvec_pushl(&gpg.args,use_format->program,+"-Y","sign",+"-n","git",+"-f",temp->filename.buf,+NULL);+}else{+strvec_pushl(&gpg.args,use_format->program,+"--status-fd=2",+"-bsau",signing_key,+NULL);+}bottom=signature->len;
@@ -464,7 +617,16 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigsignature,1024,&gpg_status,0);sigchain_pop(SIGPIPE);-ret|=!strstr(gpg_status.buf,"\n[GNUPG:] SIG_CREATED ");+if(temp)+delete_tempfile(&temp);++if(!strcmp(use_format->name,"ssh")){+if(strstr(gpg_status.buf,"unknown option")){+error(_("openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)"));+}+}else{+ret|=!strstr(gpg_status.buf,"\n[GNUPG:] SIG_CREATED ");+}strbuf_release(&gpg_status);if(ret)returnerror(_("gpg failed to sign the data"));
On Tue, Jul 6, 2021 at 10:20 AM Fabian Stelzer via GitGitGadget
[off-list ref] wrote:
From: Fabian Stelzer <redacted>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.
Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
...
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which i think is quite common (at least
for the email part). This way we can establish the correct trust for the
SSH Keys without setting up a separate GPG Infrastructure (which is
still quite painful for users) or implementing x509 signing support for
git (which lacks good forwarding mechanisms). Using ssh agent forwarding
makes this feature easily usable in todays development environments
where code is often checked out in remote VMs / containers.
Thanks for working on this, and I support this initiative. I
coincidentally have started proselytizing something similar just weeks
ago.
My interest is in signing pushes rather than commits/tags, as that (in
combination with SSH U2F support) provides a simple mechanism to
require (forwardable!) 2-factor authentication on pushes over HTTP. I
haven't looked at the signing code in detail, but I had the impression
that adding SSH signatures would automatically also add support for
signed pushes? (aka. push-certs) Do you know?
--
Han-Wen Nienhuys - Google Munich
I work 80%. Don't expect answers from me on Fridays.
--
Google Germany GmbH, Erika-Mann-Strasse 33, 80636 Munich
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
Geschäftsführer: Paul Manicle, Halimah DeLaine Prado
From: Fabian Stelzer <redacted>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.
Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
...
quoted
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which i think is quite common (at least
for the email part). This way we can establish the correct trust for the
SSH Keys without setting up a separate GPG Infrastructure (which is
still quite painful for users) or implementing x509 signing support for
git (which lacks good forwarding mechanisms). Using ssh agent forwarding
makes this feature easily usable in todays development environments
where code is often checked out in remote VMs / containers.
Thanks for working on this, and I support this initiative. I
coincidentally have started proselytizing something similar just weeks
ago.
My interest is in signing pushes rather than commits/tags, as that (in
combination with SSH U2F support) provides a simple mechanism to
require (forwardable!) 2-factor authentication on pushes over HTTP. I
haven't looked at the signing code in detail, but I had the impression
that adding SSH signatures would automatically also add support for
signed pushes? (aka. push-certs) Do you know?
Up until now i was not actually aware of the "push signing"
functionality in git.
I can see that the send/receive-pack use the same api function calls as
commit/tag signing.
So this should work just as well. Especially if using an ssh agent the whole
process is identical to git. I still need to try private key files
directly to see
how user interaction (like entering a passphrase or touching the U2F
Token) would work.
From: brian m. carlson <hidden> Date: 2021-07-06 14:45:06
On 2021-07-06 at 08:19:53, Fabian Stelzer via GitGitGadget wrote:
From: Fabian Stelzer <redacted>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.
Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
needs openssh>8.2p1
Usually we'll want to write the explanation here in full sentences with
typical capitalization.
Signed-off-by: Fabian Stelzer <redacted>
---
RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
Support for using private keyfiles directly is still missing and i'm
unsure on how to configure it or if the pubkey in the signingkey field
is such a good idea. A SSH Fingerprint as signingkey would be nicer, but
key lookup would be quite cumbersome. Maybe storing the fingerprint in
signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile
setting? As a default we could get the first ssh key from ssh-add and
store it in the config to avoid unintentional changes of the used
signing key. I've started with some tests for SSH Signing but having
static private keyfiles would make this a lot easier. So still on my
TODO.
I think user.signingKey could be helpful for signing here. That could
be a file name, not just a fingerprint, although we'd probably want to
have support for tilde expansion. You could add an additional option,
gpg.ssh.keyring, that specifies the signatures to verify. That would be
named the same thing as a potential option of gpg.openpgp.keyring,
which would be convenient. Also, gpg.ssh.revokedKeyring could maybe be
the name for revoked keys?
This feature makes git signing much more accessible to the average user.
Usually they have a SSH Key for pushing code already. Using it for
signing commits allows us to verify not only the transport but the
pushed code as well. The allowed_signers file could be kept in the
repository if all receives are verified (allowing only useris with valid
signatures to add/change them) or outside if generated/managed
differently. Tools like gitolite could optionally generate and enforce
them from the already existing user ssh keys for example.
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which i think is quite common (at least
for the email part). This way we can establish the correct trust for the
SSH Keys without setting up a separate GPG Infrastructure (which is
still quite painful for users) or implementing x509 signing support for
git (which lacks good forwarding mechanisms). Using ssh agent forwarding
makes this feature easily usable in todays development environments
where code is often checked out in remote VMs / containers.
I think some of this rationale would work well in the commit message,
especially the part about the fact that using an SSH key may be easier
for users and the fact that it can be well supported by smart cards.
Those are compelling arguments about why this is a desirable change, and
should be in the commit message.
I haven't looked too deeply at the intricacies of the change, but I'm in
favor of it. I would, however, like to see some tests here, including
for commits, tags, and push certificates. Note that you'll probably
need to run the testsuite both with and without
GIT_TEST_DEFAULT_HASH=sha256 to verify everything works.
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -27,6 +27,15 @@ gpg.minTrustLevel:: with at least `undefined` trust. Setting this option overrides the required trust-level for all operations. Supported values, in increasing order of significance:++gpg.ssh.allowedSigners::+ A file containing all valid SSH signing principals. + Similar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.+ Defaults to .gitsigners
We probably don't want to store this in the repository. If OpenSSH has
a standard location for this, then we can default to that; otherwise, we
should pick something in .ssh or in $XDG_CONFIG_HOME/git.
--
brian m. carlson (he/him or they/them)
Toronto, Ontario, CA
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.
Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
needs openssh>8.2p1
Usually we'll want to write the explanation here in full sentences with
typical capitalization.
Thanks, i was unsure about what to put in the commit and what into the
cover letter.
I'll fix this with the next patch update and move some of it into the
commit message.
In our env the commit messages are usually kept quite short.
quoted
Signed-off-by: Fabian Stelzer <redacted>
---
RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
Support for using private keyfiles directly is still missing and i'm
unsure on how to configure it or if the pubkey in the signingkey field
is such a good idea. A SSH Fingerprint as signingkey would be nicer, but
key lookup would be quite cumbersome. Maybe storing the fingerprint in
signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile
setting? As a default we could get the first ssh key from ssh-add and
store it in the config to avoid unintentional changes of the used
signing key. I've started with some tests for SSH Signing but having
static private keyfiles would make this a lot easier. So still on my
TODO.
I think user.signingKey could be helpful for signing here. That could
be a file name, not just a fingerprint, although we'd probably want to
have support for tilde expansion. You could add an additional option,
gpg.ssh.keyring, that specifies the signatures to verify. That would be
named the same thing as a potential option of gpg.openpgp.keyring,
which would be convenient. Also, gpg.ssh.revokedKeyring could maybe be
the name for revoked keys?
The problem ist that looking up a key by fingerprint alone is not really
possible with ssh :/
A referenced file (which could contain a public or private key) would be
fine and i could return the fingerprint in the get_signing_key api which
the pushcerts code uses as "pusher" info in the cert.
I'll change the keyring naming to what you suggested. Makes sense to
have this option for gpg as well.
quoted
This feature makes git signing much more accessible to the average user.
Usually they have a SSH Key for pushing code already. Using it for
signing commits allows us to verify not only the transport but the
pushed code as well. The allowed_signers file could be kept in the
repository if all receives are verified (allowing only useris with valid
signatures to add/change them) or outside if generated/managed
differently. Tools like gitolite could optionally generate and enforce
them from the already existing user ssh keys for example.
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which i think is quite common (at least
for the email part). This way we can establish the correct trust for the
SSH Keys without setting up a separate GPG Infrastructure (which is
still quite painful for users) or implementing x509 signing support for
git (which lacks good forwarding mechanisms). Using ssh agent forwarding
makes this feature easily usable in todays development environments
where code is often checked out in remote VMs / containers.
I think some of this rationale would work well in the commit message,
especially the part about the fact that using an SSH key may be easier
for users and the fact that it can be well supported by smart cards.
Those are compelling arguments about why this is a desirable change, and
should be in the commit message.
I haven't looked too deeply at the intricacies of the change, but I'm in
favor of it. I would, however, like to see some tests here, including
for commits, tags, and push certificates. Note that you'll probably
need to run the testsuite both with and without
GIT_TEST_DEFAULT_HASH=sha256 to verify everything works.
I'm working on some tests but there are lots of GPG / GPGSM tests in the
suite and i'm unsure of how many i should duplicate.
Thanks for the info with the hash setting.
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -27,6 +27,15 @@ gpg.minTrustLevel:: with at least `undefined` trust. Setting this option overrides the required trust-level for all operations. Supported values, in increasing order of significance:++gpg.ssh.allowedSigners::+ A file containing all valid SSH signing principals.+ Similar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.+ Defaults to .gitsigners
We probably don't want to store this in the repository. If OpenSSH has
a standard location for this, then we can default to that; otherwise, we
should pick something in .ssh or in $XDG_CONFIG_HOME/git.
I'm not aware of a standard location. I think there are use cases to
store this in the repo, but i'm of course fine not defaulting to it.
On 06/07/21 15.19, Fabian Stelzer via GitGitGadget wrote:
From: Fabian Stelzer <redacted>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.
Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
needs openssh>8.2p1
Why did you choose to implement SSH-based signing as GPG interface? Why
not create similar one?
If at later times we need to implement other signing methods (besides
GPG and SSH), we can refactor gpg-interface into generic signing
interface (say `signing.h`) and let each signing methods implement from it.
--
An old man doll... just what I always wanted! - Clara
On 06/07/21 15.19, Fabian Stelzer via GitGitGadget wrote:
quoted
From: Fabian Stelzer <redacted>
set gpg.format = ssh and user.signingkey to a ssh public key string
(like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.
Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
needs openssh>8.2p1
Why did you choose to implement SSH-based signing as GPG interface?
Why not create similar one?
If at later times we need to implement other signing methods (besides
GPG and SSH), we can refactor gpg-interface into generic signing
interface (say `signing.h`) and let each signing methods implement
from it.
I agree that a general purpose "signing" would be cleaner. The GPG
kewords are scattered all over the codebase but all the paths i found
just call the generic sign_buffer / verify_signed_buffer from
gpg-interface.c in the end whose api works quite well for other signing
mechanisms as well. I will rename some struct fields to be more generic
and adjust a few messages printed to the user which currently say things
like "gpg failed to sign the data" or "has a gpg signature" to be
generic. Do we just want to call this "signature" and remove the gpg
prefix or would that be too generic?
Refactoring the whole gpg part to a generic "signing" would be quite
involved and should probably be a different patch even though its mostly
renaming stuff.
If we want to go into that direction i could add the new config keys
under signing.* (signing.format = ssh|gpg, ...) and keep the
compatibility for the older gpg.* keys.
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -33,3 +33,34 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate`++gpg.ssh.keyring::+ A file containing all valid SSH public signing keys. + Similar to an .ssh/authorized_keys file.+ See ssh-keygen(1) "ALLOWED SIGNERS" for details.+ If a signing key is found in this file then the trust level will+ be set to "fully". Otherwise if the key is not present+ but the signature is still valid then the trust level will be "undefined".++ This file can be set to a location outside of the repository+ and every developer maintains their own trust store.+ A central repository server could generate this file automatically+ from ssh keys with push access to verify the code against.+ In a corporate setting this file is probably generated at a global location+ from some automation that already handles developer ssh keys. ++ A repository that is only allowing signed commits can store the file + in the repository itself using a relative path. This way only committers+ with an already valid key can add or change keys in the keyring.++ Using a SSH CA key with the cert-authority option + (see ssh-keygen(1) "CERTIFICATES") is also valid.++ To revoke a key place the public key without the principal into the + revocationKeyring.++gpg.ssh.revocationKeyring::+ Either a SSH KRL or a list of revoked public keys (without the principal prefix).+ See ssh-keygen(1) for details.+ If a public key is found in this file then it will always be treated+ as having trust level "never" and signatures will show as invalid.
@@ -36,3 +36,9 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports.+ If gpg.format is set to "ssh" this can contain the literal ssh public+ key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and + corresponds to the private key used for signing. The private key + needs to be available via ssh-agent. Alternatively it can be set to+ a file containing a private key directly. If not set git will call + "ssh-add -L" and try to use the first key available.
@@ -279,29 +342,125 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,return-1;}-fmt=get_format_by_sig(signature);-if(!fmt)-BUG("bad signature '%s'",signature);+// Find the principal from the signers+strvec_pushl(&ssh_keygen.args,fmt->program,+"-Y","find-principals",+"-f",get_ssh_allowed_signers(),+"-s",temp->filename.buf,+NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&ssh_keygen_out,0,&ssh_keygen_err,0);+if(strstr(ssh_keygen_err.buf,"unknown option")){+error(_("openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)"));+}+if(ret||!ssh_keygen_out.len){+// We did not find a matching principal in the keyring - Check without validation+child_process_init(&ssh_keygen);+strvec_pushl(&ssh_keygen.args,fmt->program,+"-Y","check-novalidate",+"-n","git",+"-s",temp->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",+//TODO: sprintf("-Overify-time=%s", commit->date...),+"-n","git",+"-f",get_ssh_allowed_signers(),+"-I",principal,+"-s",temp->filename.buf,+NULL);++if(ssh_revocation_file&&file_exists(ssh_revocation_file)){+strvec_pushl(&ssh_keygen.args,"-r",ssh_revocation_file,NULL);+}++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&ssh_keygen,payload,payload_size,+&ssh_keygen_out,0,&ssh_keygen_err,0);+sigchain_pop(SIGPIPE);++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_output = strbuf_detach(&ssh_keygen_err, NULL); // This flip around is broken...+sigc->gpg_status=strbuf_detach(&ssh_keygen_out,NULL);++parse_ssh_output(sigc);++delete_tempfile(&temp);+strbuf_release(&ssh_keygen_out);+strbuf_release(&ssh_keygen_err);++returnret;+}++staticintverify_gpg_signature(structsignature_check*sigc,structgpg_format*fmt,+constchar*payload,size_tpayload_size,+constchar*signature,size_tsignature_size)+{+structchild_processgpg=CHILD_PROCESS_INIT;+structtempfile*temp;+intret;+structstrbufgpg_out=STRBUF_INIT;+structstrbufgpg_err=STRBUF_INIT;++temp=mks_tempfile_t(".git_vtag_tmpXXXXXX");+if(!temp)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(temp->fd,signature,signature_size)<0||+close_tempfile_gently(temp)<0){+error_errno(_("failed writing detached signature to '%s'"),+temp->filename.buf);+delete_tempfile(&temp);+return-1;+}strvec_push(&gpg.args,fmt->program);strvec_pushv(&gpg.args,fmt->verify_args);strvec_pushl(&gpg.args,-"--status-fd=1",-"--verify",temp->filename.buf,"-",-NULL);--if(!gpg_status)-gpg_status=&buf;+"--status-fd=1",+"--verify",temp->filename.buf,"-",+NULL);sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,payload,payload_size,-gpg_status,0,gpg_output,0);+ret=pipe_command(&gpg,payload,payload_size,&gpg_out,0,+&gpg_err,0);sigchain_pop(SIGPIPE);+ret|=!strstr(gpg_out.buf,"\n[GNUPG:] GOODSIG ");-delete_tempfile(&temp);+sigc->payload=xmemdupz(payload,payload_size);+sigc->output=strbuf_detach(&gpg_err,NULL);+sigc->gpg_status=strbuf_detach(&gpg_out,NULL);-ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");-strbuf_release(&buf);/* no matter it was used or not */+parse_gpg_output(sigc);++delete_tempfile(&temp);+strbuf_release(&gpg_out);+strbuf_release(&gpg_err);returnret;}
@@ -388,12 +546,31 @@ int git_gpg_config(const char *var, const char *value, void *cb)intret;if(!strcmp(var,"user.signingkey")){+/* user.signingkey can contain one of the following+*whenformat=openpgp/x509+*-GPGKeyID+*whenformat=ssh+*-literalsshpublickey(e.g.ssh-rsaXXXKEYXXXcomment)+*-pathtoafilecontainingapublicoraprivatesshkey+*/if(!value)returnconfig_error_nonbool(var);set_signing_key(value);return0;}+if(!strcmp(var,"gpg.ssh.keyring")){+if(!value)+returnconfig_error_nonbool(var);+returngit_config_string(&ssh_allowed_signers,var,value);+}++if(!strcmp(var,"gpg.ssh.revocationkeyring")){+if(!value)+returnconfig_error_nonbool(var);+returngit_config_string(&ssh_revocation_file,var,value);+}+if(!strcmp(var,"gpg.format")){if(!value)returnconfig_error_nonbool(var);
@@ -433,11 +613,80 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+staticchar*get_ssh_key_fingerprint(constchar*signing_key){+structchild_processssh_keygen=CHILD_PROCESS_INIT;+intret=-1;+structstrbuffingerprint_stdout=STRBUF_INIT;+structstrbuf**fingerprint;++/* For SSH Signing this can contain a filename or a public key+*Fortextualrepresentationweusuallywantafingerprint+*/+if(istarts_with(signing_key,"ssh-")){+strvec_pushl(&ssh_keygen.args,"ssh-keygen",+"-lf","-",+NULL);+ret=pipe_command(&ssh_keygen,signing_key,strlen(signing_key),&fingerprint_stdout,0,NULL,0);+}else{+strvec_pushl(&ssh_keygen.args,"ssh-keygen",+"-lf",configured_signing_key,+NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&fingerprint_stdout,0,NULL,0);+if(!!ret)+die_errno(_("failed to get the ssh fingerprint for key '%s'"),signing_key);+fingerprint=strbuf_split_max(&fingerprint_stdout,' ',3);+if(fingerprint[1]){+returnstrbuf_detach(fingerprint[1],NULL);+}+}+die_errno(_("failed to get the ssh fingerprint for key '%s'"),signing_key);+}++// Returns the first public key from an ssh-agent to use for signing+staticchar*get_default_ssh_signing_key(void){+structchild_processssh_add=CHILD_PROCESS_INIT;+intret=-1;+structstrbufkey_stdout=STRBUF_INIT;+structstrbuf**keys;++strvec_pushl(&ssh_add.args,"ssh-add","-L",NULL);+ret=pipe_command(&ssh_add,NULL,0,&key_stdout,0,NULL,0);+if(!ret){+keys=strbuf_split_max(&key_stdout,'\n',2);+if(keys[0])+returnstrbuf_detach(keys[0],NULL);+}++return"";+}++// Returns a textual but unique representation ot the signing key+constchar*get_signing_key_id(void){+if(!strcmp(use_format->name,"ssh")){+returnget_ssh_key_fingerprint(get_signing_key());+}else{+// GPG/GPGSM only store a key id on this variable+returnget_signing_key();+}+}+constchar*get_signing_key(void){if(configured_signing_key)returnconfigured_signing_key;-returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+if(!strcmp(use_format->name,"ssh")){+returnget_default_ssh_signing_key();+}else{+returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+}+}++constchar*get_ssh_allowed_signers(void)+{+if(ssh_allowed_signers)+returnssh_allowed_signers;++die("A Path to an allowed signers ssh keyring is needed for validation");}intsign_buffer(structstrbuf*buffer,structstrbuf*signature,constchar*signing_key)
@@ -446,25 +695,88 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigintret;size_ti,j,bottom;structstrbufgpg_status=STRBUF_INIT;+structtempfile*temp=NULL,*buffer_file=NULL;+char*ssh_signing_key_file=NULL;+structstrbufssh_signature_filename=STRBUF_INIT;++if(!strcmp(use_format->name,"ssh")){+if(!signing_key||signing_key[0]=='\0')+returnerror(_("user.signingkey needs to be set for ssh signing"));+++if(istarts_with(signing_key,"ssh-")){+// A literal ssh key+temp=mks_tempfile_t(".git_signing_key_tmpXXXXXX");+if(!temp)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(temp->fd,signing_key,strlen(signing_key))<0||+close_tempfile_gently(temp)<0){+error_errno(_("failed writing ssh signing key to '%s'"),temp->filename.buf);+delete_tempfile(&temp);+return-1;+}+ssh_signing_key_file=temp->filename.buf;+}else{+// We assume a file+ssh_signing_key_file=expand_user_path(signing_key,1);+}-strvec_pushl(&gpg.args,-use_format->program,-"--status-fd=2",-"-bsau",signing_key,-NULL);+buffer_file=mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");+if(!buffer_file)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(buffer_file->fd,buffer->buf,buffer->len)<0||+close_tempfile_gently(buffer_file)<0){+error_errno(_("failed writing ssh signing key buffer to '%s'"),buffer_file->filename.buf);+delete_tempfile(&buffer_file);+return-1;+}++strvec_pushl(&gpg.args,use_format->program,+"-Y","sign",+"-n","git",+"-f",ssh_signing_key_file,+buffer_file->filename.buf,+NULL);++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&gpg,NULL,0,NULL,0,&gpg_status,0);+sigchain_pop(SIGPIPE);++strbuf_addbuf(&ssh_signature_filename,&buffer_file->filename);+strbuf_addstr(&ssh_signature_filename,".sig");+if(strbuf_read_file(signature,ssh_signature_filename.buf,2048)<0){+error_errno(_("failed reading ssh signing data buffer from '%s'"),ssh_signature_filename.buf);+}+unlink_or_warn(ssh_signature_filename.buf);+strbuf_release(&ssh_signature_filename);+delete_tempfile(&buffer_file);+}else{+strvec_pushl(&gpg.args,use_format->program,+"--status-fd=2",+"-bsau",signing_key,+NULL);++/*+*Whentheusernamesigningkeyisbad,programcouldbeterminated+*becausegpgexitswithoutreadingandthenwritegetsSIGPIPE.+*/+sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&gpg,buffer->buf,buffer->len,signature,1024,&gpg_status,0);+sigchain_pop(SIGPIPE);+}bottom=signature->len;-/*-*Whentheusernamesigningkeyisbad,programcouldbeterminated-*becausegpgexitswithoutreadingandthenwritegetsSIGPIPE.-*/-sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,buffer->buf,buffer->len,-signature,1024,&gpg_status,0);-sigchain_pop(SIGPIPE);+if(temp)+delete_tempfile(&temp);-ret|=!strstr(gpg_status.buf,"\n[GNUPG:] SIG_CREATED ");+if(!strcmp(use_format->name,"ssh")){+if(strstr(gpg_status.buf,"unknown option")){+error(_("openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)"));+}+}else{+ret|=!strstr(gpg_status.buf,"\n[GNUPG:] SIG_CREATED ");+}strbuf_release(&gpg_status);if(ret)returnerror(_("gpg failed to sign the data"));
@@ -17,8 +17,9 @@ enum signature_trust_level {structsignature_check{char*payload;-char*gpg_output;-char*gpg_status;+char*output;+char*gpg_output;// This will be printed in commit logs+char*gpg_status;// Only used internally -> remove/**possible"result":
@@ -64,6 +65,13 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,intgit_gpg_config(constchar*,constchar*,void*);voidset_signing_key(constchar*);constchar*get_signing_key(void);++/* Returns a textual unique representation of the signing key in use+*EitheraGPGKeyIDoraSSHKeyFingerprint+*/+constchar*get_signing_key_id(void);++constchar*get_ssh_allowed_signers(void);intcheck_signature(constchar*payload,size_tplen,constchar*signature,size_tslen,structsignature_check*sigc);
@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,/* could have a good signature */status=check_signature(payload.buf,payload.len,signature.buf,signature.len,&sigc);-if(sigc.gpg_output)-strbuf_addstr(&verify_message,sigc.gpg_output);+if(sigc.output)+strbuf_addstr(&verify_message,sigc.output);elsestrbuf_addstr(&verify_message,"No signature\n");signature_check_clear(&sigc);
@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'ssh signed push sends push certificate''+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.keyring"${SIGNING_KEYRING}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principal_1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'inconsistent push options in signed push not allowed''# First, invoke receive-pack with dummy input to obtain its preamble.prepare_dst&&
@@ -276,6 +323,61 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'fail without key and heed user.signingkey ssh''+test_configgpg.formatssh&&+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.keyring"${SIGNING_KEYRING}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configuser.emailhasnokey@nowhere.com&&+test_configgpg.formatssh&&++test_configuser.signingkey""&&+(+sane_unsetGIT_COMMITTER_EMAIL&&+test_must_failgitpush--signeddstnoopff+noff+)&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principal_1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'failed atomic push does not execute GPG''prepare_dst&&git-Cdstconfigreceive.certnonceseedsekrit&&
On Mon, Jul 12 2021, Fabian Stelzer via GitGitGadget wrote:
quoted hunk
gpg.format::
Specifies which key format to use when signing with `--gpg-sign`.
- Default is "openpgp" and another possible value is "x509".
+ Default is "openpgp". Other possible values are "x509", "ssh".
gpg.<format>.program::
Use this to customize the program used for the signing format you
chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
be used as a legacy synonym for `gpg.openpgp.program`. The default
- value for `gpg.x509.program` is "gpgsm".
+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
gpg.minTrustLevel::
Specifies a minimum trust level for signature verification. If
@@ -33,3 +33,34 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate`++gpg.ssh.keyring::+ A file containing all valid SSH public signing keys. + Similar to an .ssh/authorized_keys file.+ See ssh-keygen(1) "ALLOWED SIGNERS" for details.+ If a signing key is found in this file then the trust level will+ be set to "fully". Otherwise if the key is not present+ but the signature is still valid then the trust level will be "undefined".++ This file can be set to a location outside of the repository+ and every developer maintains their own trust store.+ A central repository server could generate this file automatically+ from ssh keys with push access to verify the code against.+ In a corporate setting this file is probably generated at a global location+ from some automation that already handles developer ssh keys. ++ A repository that is only allowing signed commits can store the file + in the repository itself using a relative path. This way only committers+ with an already valid key can add or change keys in the keyring.++ Using a SSH CA key with the cert-authority option + (see ssh-keygen(1) "CERTIFICATES") is also valid.++ To revoke a key place the public key without the principal into the + revocationKeyring.++gpg.ssh.revocationKeyring::+ Either a SSH KRL or a list of revoked public keys (without the principal prefix).+ See ssh-keygen(1) for details.+ If a public key is found in this file then it will always be treated+ as having trust level "never" and signatures will show as invalid.
@@ -36,3 +36,9 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports.+ If gpg.format is set to "ssh" this can contain the literal ssh public+ key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and + corresponds to the private key used for signing. The private key + needs to be available via ssh-agent. Alternatively it can be set to+ a file containing a private key directly. If not set git will call + "ssh-add -L" and try to use the first key available.
+static void parse_ssh_output(struct signature_check *sigc)
+{
+ const char *output = NULL;
+ char *next = NULL;
+
+ /* 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
+ */
Style:
/*
* Comments like this
*/
Not /* Comments [...]
+
+ output = xmemdupz(sigc->output, strcspn(sigc->output, " \n"));
+ if (skip_prefix(sigc->output, "Good \"git\" signature for ", &output)) {
+ // Valid signature for a trusted signer
We don't use C99 comments, so /* ... */ (but perhaps we should nowadays,
but that's another topic...).
+ sigc->result = 'G';
+ sigc->trust_level = TRUST_FULLY;
+
+ next = strchrnul(output, ' '); // 'principal'
+ replace_cstring(&sigc->signer, output, next);
+ output = next + 1;
+ next = strchrnul(output, ' '); // 'with'
+ output = next + 1;
+ next = strchrnul(output, ' '); // KEY Type
+ output = next + 1;
+ next = strchrnul(output, ' '); // 'key'
+ output = next + 1;
FWIW for new code we'd probably use string_list_split() or
string_list_split_in_place() or strbuf_split_buf() or something, but I
see this is following the existing pattern in the file...
@@ -279,29 +342,125 @@ static int verify_signed_buffer(const char *payload, size_t payload_size, return -1; }- fmt = get_format_by_sig(signature);- if (!fmt)- BUG("bad signature '%s'", signature);+ // Find the principal from the signers+ strvec_pushl(&ssh_keygen.args, fmt->program,+ "-Y", "find-principals",+ "-f", get_ssh_allowed_signers(),+ "-s", temp->filename.buf,+ NULL);+ ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);+ if (strstr(ssh_keygen_err.buf, "unknown option")) {+ error(_("openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)"));+ }+ if (ret || !ssh_keygen_out.len) {+ // We did not find a matching principal in the keyring - Check without validation+ child_process_init(&ssh_keygen);+ strvec_pushl(&ssh_keygen.args, fmt->program,+ "-Y", "check-novalidate",+ "-n", "git",+ "-s", temp->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')) {
Hrm, can't we use strbuf_getline() here with the underlying io_pump API
that pipe_command() uses, instead of slurping it all up, and then
splitting on '\n' ourselves? (I'm not sure)
+ 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",
+ //TODO: sprintf("-Overify-time=%s", commit->date...),
+ "-n", "git",
+ "-f", get_ssh_allowed_signers(),
+ "-I", principal,
+ "-s", temp->filename.buf,
+ NULL);
+
+ if (ssh_revocation_file && file_exists(ssh_revocation_file)) {
+ strvec_pushl(&ssh_keygen.args, "-r", ssh_revocation_file, NULL);
Do we want to silently ignore missing but configured revocation files?
So if we run this from receive-pack or whatever we'll BUG() out? I.e. I
think this should be an fsck check or something, but not a BUG(), or
does this not rely on potentially bad object-store state?
+static char *get_ssh_key_fingerprint(const char *signing_key) {
+ struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+ int ret = -1;
+ struct strbuf fingerprint_stdout = STRBUF_INIT;
+ struct strbuf **fingerprint;
+
+ /* For SSH Signing this can contain a filename or a public key
+ * For textual representation we usually want a fingerprint
+ */
+ if (istarts_with(signing_key, "ssh-")) {
+ strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+ "-lf", "-",
+ NULL);
+ ret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0, NULL, 0);
+ } else {
+ strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+ "-lf", configured_signing_key,
+ NULL);
+ ret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);
+ if (!!ret)
+ die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+ fingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);
+ if (fingerprint[1]) {
+ return strbuf_detach(fingerprint[1], NULL);
+ }
+ }
+ die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+}
Her you declare a ret that's not used at all in the "istarts_with"
branch, and we fall through to die_errno()?
[I stopped reading mostly at this point]
Perhaps you're looking for test_expect_failure for TODO tests?
I think this patch is *way* past the point of benefitting from being
split into a patch series. It grew from ~200 lines added to ~1k.
On 12.07.21 18:55, Ævar Arnfjörð Bjarmason wrote:
I'll change all the whitespace / comments / style issues with the next
commit. Thanks
quoted
+ sigc->result = 'G';
+ sigc->trust_level = TRUST_FULLY;
+
+ next = strchrnul(output, ' '); // 'principal'
+ replace_cstring(&sigc->signer, output, next);
+ output = next + 1;
+ next = strchrnul(output, ' '); // 'with'
+ output = next + 1;
+ next = strchrnul(output, ' '); // KEY Type
+ output = next + 1;
+ next = strchrnul(output, ' '); // 'key'
+ output = next + 1;
FWIW for new code we'd probably use string_list_split() or
string_list_split_in_place() or strbuf_split_buf() or something, but I
see this is following the existing pattern in the file...
I agree. This is my first patch in the git codebase so it takes a bit
getting used to all the available utilities.
@@ -279,29 +342,125 @@ static int verify_signed_buffer(const char *payload, size_t payload_size, return -1; }- fmt = get_format_by_sig(signature);- if (!fmt)- BUG("bad signature '%s'", signature);+ // Find the principal from the signers+ strvec_pushl(&ssh_keygen.args, fmt->program,+ "-Y", "find-principals",+ "-f", get_ssh_allowed_signers(),+ "-s", temp->filename.buf,+ NULL);+ ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);+ if (strstr(ssh_keygen_err.buf, "unknown option")) {+ error(_("openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)"));+ }+ if (ret || !ssh_keygen_out.len) {+ // We did not find a matching principal in the keyring - Check without validation+ child_process_init(&ssh_keygen);+ strvec_pushl(&ssh_keygen.args, fmt->program,+ "-Y", "check-novalidate",+ "-n", "git",+ "-s", temp->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')) {
Hrm, can't we use strbuf_getline() here with the underlying io_pump API
that pipe_command() uses, instead of slurping it all up, and then
splitting on '\n' ourselves? (I'm not sure)
Sounds good. I'll give it a try.
quoted
+ 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",
+ //TODO: sprintf("-Overify-time=%s", commit->date...),
+ "-n", "git",
+ "-f", get_ssh_allowed_signers(),
+ "-I", principal,
+ "-s", temp->filename.buf,
+ NULL);
+
+ if (ssh_revocation_file && file_exists(ssh_revocation_file)) {
+ strvec_pushl(&ssh_keygen.args, "-r", ssh_revocation_file, NULL);
Do we want to silently ignore missing but configured revocation files?
So if we run this from receive-pack or whatever we'll BUG() out? I.e. I
think this should be an fsck check or something, but not a BUG(), or
does this not rely on potentially bad object-store state?
The BUG() call is also from the original code. I agree that it should be
handled differently.
Unfortunately this call is also the reason that when trying to verify a
new SSH signature with a current git version you'll get a segfault from
this BUG() :/
I'm not sure if i can do anything about this other than adding a
completely new tag in the commit itself instead of "gpgsig" which might
be quite involved. I haven't looked into that too much yet.
quoted
+static char *get_ssh_key_fingerprint(const char *signing_key) {
+ struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+ int ret = -1;
+ struct strbuf fingerprint_stdout = STRBUF_INIT;
+ struct strbuf **fingerprint;
+
+ /* For SSH Signing this can contain a filename or a public key
+ * For textual representation we usually want a fingerprint
+ */
+ if (istarts_with(signing_key, "ssh-")) {
+ strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+ "-lf", "-",
+ NULL);
+ ret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0, NULL, 0);
+ } else {
+ strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+ "-lf", configured_signing_key,
+ NULL);
+ ret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);
+ if (!!ret)
+ die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+ fingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);
+ if (fingerprint[1]) {
+ return strbuf_detach(fingerprint[1], NULL);
+ }
+ }
+ die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+}
Her you declare a ret that's not used at all in the "istarts_with"
branch, and we fall through to die_errno()?
Perhaps you're looking for test_expect_failure for TODO tests?
Yes. Although this test explicitly i'm having a hard time to duplicate
for ssh. I'm still trying to find out if the duplicate signature thing
is actually an issue with ssh.
I think this patch is *way* past the point of benefitting from being
split into a patch series. It grew from ~200 lines added to ~1k.
Sure, I can easily split the patch into seperate commits. But do i
create a v3 patch from this or issue a new pull request?
The diff between v2 & v3 would be quite useless otherwise wouldn't it?
And maybe another beginner contribution question:
When i make changes to a patchset do i put new changes from the review
on top as new commits or do i edit the existing commits?
If so what is the workflow you normally use for this? fixup commits? I
know about those but haven't worked with them before.
Thanks for your help!
From: Felipe Contreras <hidden> Date: 2021-07-12 21:16:56
Fabian Stelzer wrote:
On 12.07.21 18:55, Ævar Arnfjörð Bjarmason wrote:
quoted
I think this patch is *way* past the point of benefitting from being
split into a patch series. It grew from ~200 lines added to ~1k.
Sure, I can easily split the patch into seperate commits. But do i
create a v3 patch from this or issue a new pull request?
The diff between v2 & v3 would be quite useless otherwise wouldn't it?
The interdiff might be quite useless, but not the rangediff. Either way
both of those are merely tools to visualize changes between versions,
ultimately what really matters is the final commits themselves.
Moreover, not all reviewers have seen every version, so for example if
you properly split this patch, I might join the review process at v3,
and I don't really care what was in v2, therefore I wouldn't look at the
rangediff.
And maybe another beginner contribution question:
When i make changes to a patchset do i put new changes from the review
on top as new commits or do i edit the existing commits?
Edit existing commits.
If so what is the workflow you normally use for this? fixup commits? I
know about those but haven't worked with them before.
`git rebase --interactive` is what I use, and I think that's what most
people use.
This allows you to easily edit commits and add specific changes to
specific commits.
Once you are familiar with this process it's easier to understand fixup
commits, but I'd say rebasing comes first.
Cheers.
--
Felipe Contreras
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -33,3 +33,34 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate`++gpg.ssh.keyring::+ A file containing all valid SSH public signing keys.+ Similar to an .ssh/authorized_keys file.+ See ssh-keygen(1) "ALLOWED SIGNERS" for details.+ If a signing key is found in this file then the trust level will+ be set to "fully". Otherwise if the key is not present+ but the signature is still valid then the trust level will be "undefined".++ This file can be set to a location outside of the repository+ and every developer maintains their own trust store.+ A central repository server could generate this file automatically+ from ssh keys with push access to verify the code against.+ In a corporate setting this file is probably generated at a global location+ from some automation that already handles developer ssh keys.++ A repository that is only allowing signed commits can store the file+ in the repository itself using a relative path. This way only committers+ with an already valid key can add or change keys in the keyring.++ Using a SSH CA key with the cert-authority option+ (see ssh-keygen(1) "CERTIFICATES") is also valid.++ To revoke a key place the public key without the principal into the+ revocationKeyring.++gpg.ssh.revocationKeyring::+ Either a SSH KRL or a list of revoked public keys (without the principal prefix).+ See ssh-keygen(1) for details.+ If a public key is found in this file then it will always be treated+ as having trust level "never" and signatures will show as invalid.
@@ -36,3 +36,9 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports.+ If gpg.format is set to "ssh" this can contain the literal ssh public+ key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and+ corresponds to the private key used for signing. The private key+ needs to be available via ssh-agent. Alternatively it can be set to+ a file containing a private key directly. If not set git will call+ "ssh-add -L" and try to use the first key available.
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-14 12:10:20
From: Fabian Stelzer <redacted>
Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.
Start with adding the new signature format, new config options and
rename some fields for consistency.
This feature makes git signing much more accessible to the average user.
Usually they have a SSH Key for pushing code already. Using it
for signing commits allows us to verify not only the transport but the
pushed code as well.
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which i think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.
Signed-off-by: Fabian Stelzer <redacted>
---
fmt-merge-msg.c | 4 +-
gpg-interface.c | 122 +++++++++++++++++++++++++++++++++---------------
gpg-interface.h | 5 +-
log-tree.c | 8 ++--
pretty.c | 4 +-
5 files changed, 95 insertions(+), 48 deletions(-)
@@ -279,29 +290,28 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,return-1;}-fmt=get_format_by_sig(signature);-if(!fmt)-BUG("bad signature '%s'",signature);-strvec_push(&gpg.args,fmt->program);strvec_pushv(&gpg.args,fmt->verify_args);strvec_pushl(&gpg.args,-"--status-fd=1",-"--verify",temp->filename.buf,"-",-NULL);--if(!gpg_status)-gpg_status=&buf;+"--status-fd=1",+"--verify",temp->filename.buf,"-",+NULL);sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,payload,payload_size,-gpg_status,0,gpg_output,0);+ret=pipe_command(&gpg,payload,payload_size,&gpg_out,0,+&gpg_err,0);sigchain_pop(SIGPIPE);+ret|=!strstr(gpg_out.buf,"\n[GNUPG:] GOODSIG ");-delete_tempfile(&temp);+sigc->payload=xmemdupz(payload,payload_size);+sigc->output=strbuf_detach(&gpg_err,NULL);+sigc->gpg_status=strbuf_detach(&gpg_out,NULL);-ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");-strbuf_release(&buf);/* no matter it was used or not */+parse_gpg_output(sigc);++delete_tempfile(&temp);+strbuf_release(&gpg_out);+strbuf_release(&gpg_err);returnret;}
@@ -437,7 +471,19 @@ const char *get_signing_key(void){if(configured_signing_key)returnconfigured_signing_key;-returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+if(!strcmp(use_format->name,"ssh")){+returnget_default_ssh_signing_key();+}else{+returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+}+}++constchar*get_ssh_allowed_signers(void)+{+if(ssh_allowed_signers)+returnssh_allowed_signers;++die("A Path to an allowed signers ssh keyring is needed for validation");}intsign_buffer(structstrbuf*buffer,structstrbuf*signature,constchar*signing_key)
@@ -17,8 +17,8 @@ enum signature_trust_level {structsignature_check{char*payload;-char*gpg_output;-char*gpg_status;+char*output;+char*gpg_status;/* Only used internally -> remove from this public api *//**possible"result":
@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,/* could have a good signature */status=check_signature(payload.buf,payload.len,signature.buf,signature.len,&sigc);-if(sigc.gpg_output)-strbuf_addstr(&verify_message,sigc.gpg_output);+if(sigc.output)+strbuf_addstr(&verify_message,sigc.output);elsestrbuf_addstr(&verify_message,"No signature\n");signature_check_clear(&sigc);
@@ -467,6 +467,23 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+/* Returns the first public key from an ssh-agent to use for signing */+staticchar*get_default_ssh_signing_key(void){+structchild_processssh_add=CHILD_PROCESS_INIT;+intret=-1;+structstrbufkey_stdout=STRBUF_INIT;+structstrbuf**keys;++strvec_pushl(&ssh_add.args,"ssh-add","-L",NULL);+ret=pipe_command(&ssh_add,NULL,0,&key_stdout,0,NULL,0);+if(!ret){+keys=strbuf_split_max(&key_stdout,'\n',2);+if(keys[0])+returnstrbuf_detach(keys[0],NULL);+}++return"";+}constchar*get_signing_key(void){if(configured_signing_key)
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-14 12:10:25
From: Fabian Stelzer <redacted>
for ssh the key can be a filename/path or even a literal ssh pubkey
in push certs and textual output we prefer the ssh fingerprint instead
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++
gpg-interface.h | 7 +++++++
send-pack.c | 8 ++++----
3 files changed, 58 insertions(+), 4 deletions(-)
@@ -467,6 +467,42 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+staticchar*get_ssh_key_fingerprint(constchar*signing_key){+structchild_processssh_keygen=CHILD_PROCESS_INIT;+intret=-1;+structstrbuffingerprint_stdout=STRBUF_INIT;+structstrbuf**fingerprint;++/*+*WithSSHSigningthiscancontainafilenameorapublickey+*Fortextualrepresentationweusuallywantafingerprint+*/+if(istarts_with(signing_key,"ssh-")){+strvec_pushl(&ssh_keygen.args,"ssh-keygen",+"-lf","-",+NULL);+ret=pipe_command(&ssh_keygen,signing_key,strlen(signing_key),+&fingerprint_stdout,0,NULL,0);+}else{+strvec_pushl(&ssh_keygen.args,"ssh-keygen",+"-lf",configured_signing_key,+NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&fingerprint_stdout,0,+NULL,0);+}++if(!!ret)+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++fingerprint=strbuf_split_max(&fingerprint_stdout,' ',3);+if(!fingerprint[1])+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++returnstrbuf_detach(fingerprint[1],NULL);+}+/* Returns the first public key from an ssh-agent to use for signing */staticchar*get_default_ssh_signing_key(void){structchild_processssh_add=CHILD_PROCESS_INIT;
@@ -484,6 +520,17 @@ static char *get_default_ssh_signing_key(void) {return"";}++/* Returns a textual but unique representation ot the signing key */+constchar*get_signing_key_id(void){+if(!strcmp(use_format->name,"ssh")){+returnget_ssh_key_fingerprint(get_signing_key());+}else{+/* GPG/GPGSM only store a key id on this variable */+returnget_signing_key();+}+}+constchar*get_signing_key(void){if(configured_signing_key)
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,{conststructref*ref;structstring_list_item*item;-char*signing_key=xstrdup(get_signing_key());+char*signing_key_id=xstrdup(get_signing_key_id());constchar*cp,*np;structstrbufcert=STRBUF_INIT;intupdate_seen=0;strbuf_addstr(&cert,"certificate version 0.1\n");-strbuf_addf(&cert,"pusher %s ",signing_key);+strbuf_addf(&cert,"pusher %s ",signing_key_id);datestamp(&cert);strbuf_addch(&cert,'\n');if(args->url&&*args->url){
@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,if(!update_seen)gotofree_return;-if(sign_buffer(&cert,&cert,signing_key))+if(sign_buffer(&cert,&cert,get_signing_key()))die(_("failed to sign the push certificate"));packet_buf_write(req_buf,"push-cert%c%s",0,cap_string);
@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,packet_buf_write(req_buf,"push-cert-end\n");free_return:-free(signing_key);+free(signing_key_id);strbuf_release(&cert);returnupdate_seen;}
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-14 12:10:25
From: Fabian Stelzer <redacted>
implements the actual ssh-keygen -Y sign operation
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.
If no signingkey is set then git will call 'ssh-add -L' to check for
available agent keys and use the first one for signing.
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 86 +++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 76 insertions(+), 10 deletions(-)
@@ -505,30 +505,96 @@ const char *get_ssh_allowed_signers(void)intsign_buffer(structstrbuf*buffer,structstrbuf*signature,constchar*signing_key){-structchild_processgpg=CHILD_PROCESS_INIT;+structchild_processsigner=CHILD_PROCESS_INIT;intret;size_ti,j,bottom;-structstrbufgpg_status=STRBUF_INIT;+structstrbufsigner_stderr=STRBUF_INIT;+structtempfile*temp=NULL,*buffer_file=NULL;+char*ssh_signing_key_file=NULL;+structstrbufssh_signature_filename=STRBUF_INIT;-strvec_pushl(&gpg.args,-use_format->program,+if(!strcmp(use_format->name,"ssh")){+if(!signing_key||signing_key[0]=='\0')+returnerror(_("user.signingkey needs to be set for ssh signing"));+++if(istarts_with(signing_key,"ssh-")){+/* A literal ssh key */+temp=mks_tempfile_t(".git_signing_key_tmpXXXXXX");+if(!temp)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(temp->fd,signing_key,strlen(signing_key))<0||+close_tempfile_gently(temp)<0){+error_errno(_("failed writing ssh signing key to '%s'"),+temp->filename.buf);+delete_tempfile(&temp);+return-1;+}+ssh_signing_key_file=temp->filename.buf;+}else{+/* We assume a file */+ssh_signing_key_file=expand_user_path(signing_key,1);+}++buffer_file=mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");+if(!buffer_file)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(buffer_file->fd,buffer->buf,buffer->len)<0||+close_tempfile_gently(buffer_file)<0){+error_errno(_("failed writing ssh signing key buffer to '%s'"),+buffer_file->filename.buf);+delete_tempfile(&buffer_file);+return-1;+}++strvec_pushl(&signer.args,use_format->program,+"-Y","sign",+"-n","git",+"-f",ssh_signing_key_file,+buffer_file->filename.buf,+NULL);++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&signer,NULL,0,NULL,0,&signer_stderr,0);+sigchain_pop(SIGPIPE);++strbuf_addbuf(&ssh_signature_filename,&buffer_file->filename);+strbuf_addstr(&ssh_signature_filename,".sig");+if(strbuf_read_file(signature,ssh_signature_filename.buf,2048)<0){+error_errno(_("failed reading ssh signing data buffer from '%s'"),+ssh_signature_filename.buf);+}+unlink_or_warn(ssh_signature_filename.buf);+strbuf_release(&ssh_signature_filename);+delete_tempfile(&buffer_file);+}else{+strvec_pushl(&signer.args,use_format->program,"--status-fd=2","-bsau",signing_key,NULL);-bottom=signature->len;-/**Whentheusernamesigningkeyisbad,programcouldbeterminated*becausegpgexitswithoutreadingandthenwritegetsSIGPIPE.*/sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,buffer->buf,buffer->len,-signature,1024,&gpg_status,0);+ret=pipe_command(&signer,buffer->buf,buffer->len,signature,1024,&signer_stderr,0);sigchain_pop(SIGPIPE);+}++bottom=signature->len;++if(temp)+delete_tempfile(&temp);-ret|=!strstr(gpg_status.buf,"\n[GNUPG:] SIG_CREATED ");-strbuf_release(&gpg_status);+if(!strcmp(use_format->name,"ssh")){+if(strstr(signer_stderr.buf,"usage:")){+error(_("openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)"));+}+}else{+ret|=!strstr(signer_stderr.buf,"\n[GNUPG:] SIG_CREATED ");+}+strbuf_release(&signer_stderr);if(ret)returnerror(_("gpg failed to sign the data"));
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-14 12:10:27
From: Fabian Stelzer <redacted>
Verification uses the gpg.ssh.keyring file (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 keyring file 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 <redacted>
---
builtin/receive-pack.c | 2 +
gpg-interface.c | 139 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+)
@@ -156,6 +157,42 @@ static int parse_gpg_trust_level(const char *level,return1;}+staticvoidparse_ssh_output(structsignature_check*sigc)+{+structstring_listparts=STRING_LIST_INIT_DUP;+char*line=NULL;++/*+*ssh-keysignoutputshouldbe:+*Good"git"signatureforPRINCIPALwithRSAkeySHA256:FINGERPRINT+*orforvalidbutunknownkeys:+*Good"git"signaturewithRSAkeySHA256:FINGERPRINT+*/+sigc->result='B';+sigc->trust_level=TRUST_NEVER;++line=xmemdupz(sigc->output,strcspn(sigc->output,"\n"));+string_list_split(&parts,line,' ',8);+if(parts.nr>=9&&starts_with(line,"Good \"git\" signature for ")){+/* Valid signature for a trusted signer */+sigc->result='G';+sigc->trust_level=TRUST_FULLY;+sigc->signer=xstrdup(parts.items[4].string);+sigc->fingerprint=xstrdup(parts.items[8].string);+sigc->key=xstrdup(sigc->fingerprint);+}elseif(parts.nr>=7&&starts_with(line,"Good \"git\" signature with ")){+/* Valid signature, but key unknown */+sigc->result='G';+sigc->trust_level=TRUST_UNDEFINED;+sigc->fingerprint=xstrdup(parts.items[6].string);+sigc->key=xstrdup(sigc->fingerprint);+}+trace_printf("trace: sigc result %c/%d - %s %s %s",sigc->result,sigc->trust_level,sigc->signer,sigc->fingerprint,sigc->key);++string_list_clear(&parts,0);+FREE_AND_NULL(line);+}+staticvoidparse_gpg_output(structsignature_check*sigc){constchar*buf=sigc->gpg_status;
@@ -269,6 +306,108 @@ error:FREE_AND_NULL(sigc->key);}+staticintverify_ssh_signature(structsignature_check*sigc,+structgpg_format*fmt,+constchar*payload,size_tpayload_size,+constchar*signature,size_tsignature_size)+{+structchild_processssh_keygen=CHILD_PROCESS_INIT;+structtempfile*temp;+intret;+constchar*line;+size_ttrust_size;+char*principal;+structstrbufssh_keygen_out=STRBUF_INIT;+structstrbufssh_keygen_err=STRBUF_INIT;++temp=mks_tempfile_t(".git_vtag_tmpXXXXXX");+if(!temp)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(temp->fd,signature,signature_size)<0||+close_tempfile_gently(temp)<0){+error_errno(_("failed writing detached signature to '%s'"),+temp->filename.buf);+delete_tempfile(&temp);+return-1;+}++/* Find the principal from the signers */+strvec_pushl(&ssh_keygen.args,fmt->program,+"-Y","find-principals",+"-f",get_ssh_allowed_signers(),+"-s",temp->filename.buf,+NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&ssh_keygen_out,0,&ssh_keygen_err,0);+if(strstr(ssh_keygen_err.buf,"usage:")){+error(_("openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)"));+}+if(ret||!ssh_keygen_out.len){+/* We did not find a matching principal in the keyring - Check without validation */+child_process_init(&ssh_keygen);+strvec_pushl(&ssh_keygen.args,fmt->program,+"-Y","check-novalidate",+"-n","git",+"-s",temp->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",get_ssh_allowed_signers(),+"-I",principal,+"-s",temp->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);++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);++delete_tempfile(&temp);+strbuf_release(&ssh_keygen_out);+strbuf_release(&ssh_keygen_err);++returnret;+}+staticintverify_gpg_signature(structsignature_check*sigc,structgpg_format*fmt,constchar*payload,size_tpayload_size,constchar*signature,size_tsignature_size)
@@ -0,0 +1,398 @@+#!/bin/sh++test_description='ssh signed commit tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+GNUPGHOME_NOT_USED=$GNUPGHOME+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed commits''+test_oid_cache<<-\EOF&&+headersha1:gpgsig+headersha256:gpgsig-sha256+EOF++test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-S-minitial&&+gittaginitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-S-msecond&&+gittagsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittagmerge&&++echo4>file&&test_tick&&gitcommit-a-m"fourth unsigned"&&+gittagfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittagfourth-signed&&++gitconfigcommit.gpgsigntrue&&+echo5>file&&test_tick&&gitcommit-a-m"fifth signed"&&+gittagfifth-signed&&++gitconfigcommit.gpgsignfalse&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittagsixth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo7>file&&test_tick&&gitcommit-a-m"seventh"--no-gpg-sign&&+gittagseventh-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittagsixth-signedHEAD^&&+gittagseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth-S"${SIGNING_KEY_UNTRUSTED}"&&+gittageighth-signed-alt&&++# commit.gpgsign is still on but this must not be signed+echo9|gitcommit-treeHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagninth-unsigned$(catoid)&&+# explicit -S of course must sign.+echo10|gitcommit-tree-SHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtenth-signed$(catoid)&&++# --gpg-sign[=<key-id>] must sign.+echo11|gitcommit-tree--gpg-signHEAD^{tree}>oid&&+test_line_count=1oid&&+gittageleventh-signed$(catoid)&&+echo12|gitcommit-tree--gpg-sign="${SIGNING_KEY_UNTRUSTED}"HEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtwelfth-signed-alt$(catoid)+'++test_expect_successGPGSSH'verify and show signatures''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+test_configgpg.mintrustlevelUNDEFINED&&+(+forcommitininitialsecondmergefourth-signed\+fifth-signedsixth-signedseventh-signedtenth-signed\+eleventh-signed+do+gitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsigned\+seventh-unsignedninth-unsigned+do+test_must_failgitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alttwelfth-signed-alt+do+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'verify-commit exits success on untrusted signature''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+gitverify-commiteighth-signed-alt2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual+'++test_expect_successGPGSSH'verify-commit exits success with matching minTrustLevel''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+test_configgpg.minTrustLevelfully&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits success with low minTrustLevel''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+test_configgpg.minTrustLevelmarginal&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits failure with high minTrustLevel''+test_configgpg.minTrustLevelultimate&&+test_must_failgitverify-commiteighth-signed-alt+'++test_expect_successGPGSSH'verify signatures with --raw''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+(+forcommitininitialsecondmergefourth-signedfifth-signedsixth-signedseventh-signed+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsignedseventh-unsigned+do+test_must_failgitverify-commit--raw$commit2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alt+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'proper header is used for hash algorithm''+gitcat-filecommitfourth-signed>output&&+grep"^$(test_oidheader) -----BEGIN SSH SIGNATURE-----"output+'++test_expect_successGPGSSH'show signed commit with signature''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+gitshow-sinitial>commit&&+gitshow-s--show-signatureinitial>show&&+gitverify-commit-vinitial>verify.12>verify.2&&+gitcat-filecommitinitial>cat&&+grep-v-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.commit&&+grep-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.gpg&&+grep-v"^ "cat|grep-v"^gpgsig.* ">cat.commit&&+test_cmpshow.commitcommit&&+test_cmpshow.gpgverify.2&&+test_cmpcat.commitverify.1+'++test_expect_successGPGSSH'detect fudged signature''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+gitcat-filecommitseventh-signed>raw&&+sed-e"s/^seventh/7th forged/"raw>forged1&&+githash-object-w-tcommitforged1>forged1.commit&&+test_must_failgitverify-commit$(catforged1.commit)&&+gitshow--pretty=short--show-signature$(catforged1.commit)>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'detect fudged signature with NUL''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+gitcat-filecommitseventh-signed>raw&&+catraw>forged2&&+echoQwik|tr"Q""\000">>forged2&&+githash-object-w-tcommitforged2>forged2.commit&&+test_must_failgitverify-commit$(catforged2.commit)&&+gitshow--pretty=short--show-signature$(catforged2.commit)>actual2&&+grep"${BAD_SIGNATURE}"actual2&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual2+'++test_expect_successGPGSSH'amending already signed commit''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+gitcheckoutfourth-signed^0&&+gitcommit--amend-S--no-edit&&+gitverify-commitHEAD&&+gitshow-s--show-signatureHEAD>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual+'++test_expect_successGPGSSH'show good signature with custom format''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+cat>expect.tmpl<<-\EOF&&+G+FINGERPRINT+principal_1+FINGERPRINT++EOF+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show bad signature with custom format''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+cat>expect<<-\EOF&&+B+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catforged1.commit)>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with custom format''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+cat>expect.tmpl<<-\EOF&&+U+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with undefined trust level''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+cat>expect.tmpl<<-\EOF&&+undefined+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with ultimate trust level''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+cat>expect.tmpl<<-\EOF&&+fully+FINGERPRINT+principal_1+FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show lack of signature with custom format''+cat>expect<<-\EOF&&+N+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"seventh-unsigned>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'log.showsignature behaves like --show-signature''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+test_configlog.showsignaturetrue&&+gitshowinitial>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual+'++test_expect_successGPGSSH'check config gpg.format values''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.formatssh&&+gitcommit-S--amend-m"success"&&+test_configgpg.formatOpEnPgP&&+test_must_failgitcommit-S--amend-m"fail"+'++test_expect_failureGPGSSH'detect fudged commit with double signature (TODO)''+sed-e"/gpgsig/,/END PGP/d"forged1>double-base&&+sed-n-e"/gpgsig/,/END PGP/p"forged1|\+sed-e"s/^$(test_oidheader)//;s/^ //"|gpg--dearmor>double-sig1.sig&&+gpg-odouble-sig2.sig-u29472784--detach-signdouble-base&&+catdouble-sig1.sigdouble-sig2.sig|gpg--enarmor>double-combined.asc&&+sed-e"s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oidheader) /;2,\$s/^/ /"\+double-combined.asc>double-gpgsig&&+sed-e"/committer/r double-gpgsig"double-base>double-commit&&+githash-object-w-tcommitdouble-commit>double-commit.commit&&+test_must_failgitverify-commit$(catdouble-commit.commit)&&+gitshow--pretty=short--show-signature$(catdouble-commit.commit)>double-actual&&+grep"BAD signature from"double-actual&&+grep"Good signature from"double-actual+'++test_expect_failureGPGSSH'show double signature with custom format (TODO)''+cat>expect<<-\EOF&&+E+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catdouble-commit.commit)>actual&&+test_cmpexpectactual+'+++test_expect_failureGPGSSH'verify-commit verifies multiply signed commits (TODO)''+gitinitmultiply-signed&&+cdmultiply-signed&&+test_commitfirst&&+echo1>second&&+gitaddsecond&&+tree=$(gitwrite-tree)&&+parent=$(gitrev-parseHEAD^{commit})&&+gitcommit--gpg-sign-msecond&&+gitcat-filecommitHEAD&&+# Avoid trailing whitespace.+sed-e"s/^Q//"-e"s/^Z/ /">commit<<-EOF&&+Qtree$tree+Qparent$parent+QauthorAUThor<author@example.com>1112912653-0700+QcommitterCOMitter<committer@example.com>1112912653-0700+Qgpgsig-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC+QAKCTqBh+VabJceXcGIZuF0Ry+udbBQ==+Q=tQ0N+Q-----ENDPGPSIGNATURE-----+Qgpgsig-sha256-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO+QAJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==+Q=pIwP+Q-----ENDPGPSIGNATURE-----+Q+Qsecond+EOF+head=$(githash-object-tcommit-wcommit)&&+gitreset--hard$head&&+gitverify-commit$head2>actual&&+grep"Good signature from"actual&&+!grep"BAD signature from"actual+'++test_done
@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'ssh signed push sends push certificate''+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.keyring"${SIGNING_KEYRING}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principal_1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'inconsistent push options in signed push not allowed''# First, invoke receive-pack with dummy input to obtain its preamble.prepare_dst&&
@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'fail without key and heed user.signingkey ssh''+test_configgpg.formatssh&&+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.keyring"${SIGNING_KEYRING}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configuser.emailhasnokey@nowhere.com&&+test_configgpg.formatssh&&+test_configuser.signingkey""&&+(+sane_unsetGIT_COMMITTER_EMAIL&&+test_must_failgitpush--signeddstnoopff+noff+)&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principal_1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'failed atomic push does not execute GPG''prepare_dst&&git-Cdstconfigreceive.certnonceseedsekrit&&
@@ -0,0 +1,161 @@+#!/bin/sh++test_description='signed tag tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed tags ssh''+test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-minitial&&+gittag-s-minitialinitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-msecond&&+gittag-s-msecondsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittag-s-mmergemerge&&++echo4>file&&test_tick&&gitcommit-a-S-m"fourth unsigned"&&+gittag-a-mfourth-unsignedfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittag-s-mfourthfourth-signed&&++echo5>file&&test_tick&&gitcommit-a-m"fifth"&&+gittagfifth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittag-a-msixthsixth-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittag-s-m6thsixth-signedHEAD^&&+gittag-mseventh-sseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth&&+gittag-u"${SIGNING_KEY_UNTRUSTED}"-meightheighth-signed-alt+'++test_expect_successGPGSSH'verify and show ssh signatures''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'detect fudged ssh signature''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+gitcat-filetagseventh-signed>raw&&+sed-e"/^tag / s/seventh/7th forged/"raw>forged1&&+githash-object-w-ttagforged1>forged1.tag&&+test_must_failgitverify-tag$(catforged1.tag)2>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'verify ssh signatures with --raw''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag--raw$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'verify signatures with --raw ssh''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+gitverify-tag--rawsixth-signed2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echosixth-signedOK+'++test_expect_successGPGSSH'verify multiple tags ssh''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+tags="seventh-signed sixth-signed"&&+foriin$tags+do+gitverify-tag-v--raw$i||return1+done>expect.stdout2>expect.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<expect.stderr.1>expect.stderr&&+gitverify-tag-v--raw$tags>actual.stdout2>actual.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<actual.stderr.1>actual.stderr&&+test_cmpexpect.stdoutactual.stdout&&+test_cmpexpect.stderractual.stderr+'++test_expect_successGPGSSH'verifying tag with --format - ssh''+test_configgpg.ssh.keyring"${SIGNING_KEYRING}"&&+cat>expect<<-\EOF&&+tagname:fourth-signed+EOF+gitverify-tag--format="tagname : %(tag)""fourth-signed">actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'verifying a forged tag with --format should fail silently - ssh''+test_must_failgitverify-tag--format="tagname : %(tag)"$(catforged1.tag)>actual-forged&&+test_must_be_emptyactual-forged+'++test_done
From: Gwyneth Morgan <hidden> Date: 2021-07-16 00:16:44
On 2021-07-14 12:10:10+0000, Fabian Stelzer via GitGitGadget wrote:
+ 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);
This breaks on principals with spaces in them (principals in the allowed
signers file can have spaces if surrounded by quotes). Looks like
strcspn should reject "\n" instead of " \n".
BTW, thanks for working on this feature. It seems much more convenient
than GPG in my testing.
On 2021-07-14 12:10:10+0000, Fabian Stelzer via GitGitGadget wrote:
quoted
+ 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);
This breaks on principals with spaces in them (principals in the allowed
signers file can have spaces if surrounded by quotes). Looks like
strcspn should reject "\n" instead of " \n".
BTW, thanks for working on this feature. It seems much more convenient
than GPG in my testing.
Oh thanks. Very nice catch. Easily fixed here but i'll have to rewrite
the verification output parsing to account for this as well.
I will add a testcase too.
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-19 13:33:25
From: Fabian Stelzer <redacted>
Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.
In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.
To be able to implement new signing formats this commit:
- makes the sigc structure more generic by renaming "gpg_output" to
"output"
- introduces function pointers in the gpg_format structure to call
format specific signing and verification functions
- moves format detection from verify_signed_buffer into the check_signature
api function and calls the format specific verify
- renames and wraps sign_buffer to handle format specific signing logic
as well
Signed-off-by: Fabian Stelzer <redacted>
---
fmt-merge-msg.c | 6 +--
gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
gpg-interface.h | 2 +-
log-tree.c | 8 ++--
pretty.c | 4 +-
5 files changed, 74 insertions(+), 50 deletions(-)
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,"--verify",temp->filename.buf,"-",NULL);-if(!gpg_status)-gpg_status=&buf;-sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,payload,payload_size,-gpg_status,0,gpg_output,0);+ret=pipe_command(&gpg,payload,payload_size,&gpg_stdout,0,+&gpg_stderr,0);sigchain_pop(SIGPIPE);delete_tempfile(&temp);-ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");-strbuf_release(&buf);/* no matter it was used or not */+ret|=!strstr(gpg_stdout.buf,"\n[GNUPG:] GOODSIG ");+sigc->payload=xmemdupz(payload,payload_size);+sigc->output=strbuf_detach(&gpg_stderr,NULL);+sigc->gpg_status=strbuf_detach(&gpg_stdout,NULL);++parse_gpg_output(sigc);++strbuf_release(&gpg_stdout);+strbuf_release(&gpg_stderr);returnret;}
@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,/* could have a good signature */status=check_signature(payload.buf,payload.len,signature.buf,signature.len,&sigc);-if(sigc.gpg_output)-strbuf_addstr(&verify_message,sigc.gpg_output);+if(sigc.output)+strbuf_addstr(&verify_message,sigc.output);elsestrbuf_addstr(&verify_message,"No signature\n");signature_check_clear(&sigc);
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-19 13:33:30
From: Fabian Stelzer <redacted>
implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.
gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 129 insertions(+), 8 deletions(-)
@@ -494,13 +527,101 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,returnerror(_("gpg failed to sign the data"));/* Strip CR from the line endings, in case we are on Windows. */-for(i=j=bottom;i<signature->len;i++)-if(signature->buf[i]!='\r'){-if(i!=j)-signature->buf[j]=signature->buf[i];-j++;-}-strbuf_setlen(signature,j);+strbuf_trim_trailing_cr(signature,bottom);return0;}++staticintsign_buffer_ssh(structstrbuf*buffer,structstrbuf*signature,+constchar*signing_key)+{+structchild_processsigner=CHILD_PROCESS_INIT;+intret=-1;+size_tbottom;+structstrbufsigner_stderr=STRBUF_INIT;+structtempfile*temp=NULL,*buffer_file=NULL;+char*ssh_signing_key_file=NULL;+structstrbufssh_signature_filename=STRBUF_INIT;++if(!signing_key||signing_key[0]=='\0')+returnerror(+_("user.signingkey needs to be set for ssh signing"));++if(istarts_with(signing_key,"ssh-")){+/* A literal ssh key */+temp=mks_tempfile_t(".git_signing_key_tmpXXXXXX");+if(!temp)+returnerror_errno(+_("could not create temporary file"));+if(write_in_full(temp->fd,signing_key,strlen(signing_key))<+0||+close_tempfile_gently(temp)<0){+error_errno(_("failed writing ssh signing key to '%s'"),+temp->filename.buf);+gotoout;+}+ssh_signing_key_file=temp->filename.buf;+}else{+/* We assume a file */+ssh_signing_key_file=expand_user_path(signing_key,1);+}++buffer_file=mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");+if(!buffer_file){+error_errno(_("could not create temporary file"));+gotoout;+}++if(write_in_full(buffer_file->fd,buffer->buf,buffer->len)<0||+close_tempfile_gently(buffer_file)<0){+error_errno(_("failed writing ssh signing key buffer to '%s'"),+buffer_file->filename.buf);+gotoout;+}++strvec_pushl(&signer.args,use_format->program,"-Y","sign","-n",+"git","-f",ssh_signing_key_file,+buffer_file->filename.buf,NULL);++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&signer,NULL,0,NULL,0,&signer_stderr,0);+sigchain_pop(SIGPIPE);++if(ret&&strstr(signer_stderr.buf,"usage:")){+error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));+gotoout;+}++if(ret){+error("%s",signer_stderr.buf);+gotoout;+}++bottom=signature->len;++strbuf_addbuf(&ssh_signature_filename,&buffer_file->filename);+strbuf_addstr(&ssh_signature_filename,".sig");+if(strbuf_read_file(signature,ssh_signature_filename.buf,2048)<0){+error_errno(+_("failed reading ssh signing data buffer from '%s'"),+ssh_signature_filename.buf);+}+unlink_or_warn(ssh_signature_filename.buf);++if(ret){+error(_("ssh failed to sign the data"));+gotoout;+}++/* Strip CR from the line endings, in case we are on Windows. */+strbuf_trim_trailing_cr(signature,bottom);++out:+if(temp)+delete_tempfile(&temp);+if(buffer_file)+delete_tempfile(&buffer_file);+strbuf_release(&signer_stderr);+strbuf_release(&ssh_signature_filename);+returnret;+}
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-19 13:33:31
From: Fabian Stelzer <redacted>
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+/* Returns the first public key from an ssh-agent to use for signing */+staticchar*get_default_ssh_signing_key(void)+{+structchild_processssh_add=CHILD_PROCESS_INIT;+intret=-1;+structstrbufkey_stdout=STRBUF_INIT;+structstrbuf**keys;++strvec_pushl(&ssh_add.args,"ssh-add","-L",NULL);+ret=pipe_command(&ssh_add,NULL,0,&key_stdout,0,NULL,0);+if(!ret){+keys=strbuf_split_max(&key_stdout,'\n',2);+if(keys[0])+returnstrbuf_detach(keys[0],NULL);+}++strbuf_release(&key_stdout);+return"";+}+constchar*get_signing_key(void){if(configured_signing_key)returnconfigured_signing_key;-returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+if(!strcmp(use_format->name,"ssh")){+returnget_default_ssh_signing_key();+}else{+returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+}}intsign_buffer(structstrbuf*buffer,structstrbuf*signature,constchar*signing_key)
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-19 13:33:32
From: Fabian Stelzer <redacted>
for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.
in push certs and textual output we prefer the ssh fingerprint instead.
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
gpg-interface.h | 6 ++++++
send-pack.c | 8 ++++----
3 files changed, 56 insertions(+), 4 deletions(-)
@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+staticchar*get_ssh_key_fingerprint(constchar*signing_key)+{+structchild_processssh_keygen=CHILD_PROCESS_INIT;+intret=-1;+structstrbuffingerprint_stdout=STRBUF_INIT;+structstrbuf**fingerprint;++/*+*WithSSHSigningthiscancontainafilenameorapublickey+*Fortextualrepresentationweusuallywantafingerprint+*/+if(istarts_with(signing_key,"ssh-")){+strvec_pushl(&ssh_keygen.args,"ssh-keygen","-lf","-",NULL);+ret=pipe_command(&ssh_keygen,signing_key,+strlen(signing_key),&fingerprint_stdout,0,+NULL,0);+}else{+strvec_pushl(&ssh_keygen.args,"ssh-keygen","-lf",+configured_signing_key,NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&fingerprint_stdout,0,+NULL,0);+}++if(!!ret)+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++fingerprint=strbuf_split_max(&fingerprint_stdout,' ',3);+if(!fingerprint[1])+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++returnstrbuf_detach(fingerprint[1],NULL);+}+/* Returns the first public key from an ssh-agent to use for signing */staticchar*get_default_ssh_signing_key(void){
@@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)return"";}+/* Returns a textual but unique representation ot the signing key */+constchar*get_signing_key_id(void)+{+if(!strcmp(use_format->name,"ssh")){+returnget_ssh_key_fingerprint(get_signing_key());+}else{+/* GPG/GPGSM only store a key id on this variable */+returnget_signing_key();+}+}+constchar*get_signing_key(void){if(configured_signing_key)
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,{conststructref*ref;structstring_list_item*item;-char*signing_key=xstrdup(get_signing_key());+char*signing_key_id=xstrdup(get_signing_key_id());constchar*cp,*np;structstrbufcert=STRBUF_INIT;intupdate_seen=0;strbuf_addstr(&cert,"certificate version 0.1\n");-strbuf_addf(&cert,"pusher %s ",signing_key);+strbuf_addf(&cert,"pusher %s ",signing_key_id);datestamp(&cert);strbuf_addch(&cert,'\n');if(args->url&&*args->url){
@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,if(!update_seen)gotofree_return;-if(sign_buffer(&cert,&cert,signing_key))+if(sign_buffer(&cert,&cert,get_signing_key()))die(_("failed to sign the push certificate"));packet_buf_write(req_buf,"push-cert%c%s",0,cap_string);
@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,packet_buf_write(req_buf,"push-cert-end\n");free_return:-free(signing_key);+free(signing_key_id);strbuf_release(&cert);returnupdate_seen;}
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-19 13:33:33
From: Fabian Stelzer <redacted>
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.
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 <redacted>
---
builtin/receive-pack.c | 2 +
gpg-interface.c | 174 ++++++++++++++++++++++++++++++++++++++++-
2 files changed, 175 insertions(+), 1 deletion(-)
@@ -343,6 +349,160 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,returnret;}+staticvoidparse_ssh_output(structsignature_check*sigc)+{+constchar*line,*principal,*search;++/*+*ssh-keysignoutputshouldbe:+*Good"git"signatureforPRINCIPALwithRSAkeySHA256:FINGERPRINT+*Good"git"signatureforPRINCIPALWITHWHITESPACEwithRSAkeySHA256:FINGERPRINT+*orforvalidbutunknownkeys:+*Good"git"signaturewithRSAkeySHA256: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);+}elseif(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);+}+}++staticconstchar*get_ssh_allowed_signers(void)+{+if(ssh_allowed_signers)+returnssh_allowed_signers;++die("gpg.ssh.allowedSignersFile needs to be configured and exist for validation");+}++staticintverify_ssh_signed_buffer(structsignature_check*sigc,+structgpg_format*fmt,constchar*payload,+size_tpayload_size,constchar*signature,+size_tsignature_size)+{+structchild_processssh_keygen=CHILD_PROCESS_INIT;+structtempfile*temp;+intret;+constchar*line;+size_ttrust_size;+char*principal;+structstrbufssh_keygen_out=STRBUF_INIT;+structstrbufssh_keygen_err=STRBUF_INIT;++temp=mks_tempfile_t(".git_vtag_tmpXXXXXX");+if(!temp)+returnerror_errno(_("could not create temporary file"));+if(write_in_full(temp->fd,signature,signature_size)<0||+close_tempfile_gently(temp)<0){+error_errno(_("failed writing detached signature to '%s'"),+temp->filename.buf);+delete_tempfile(&temp);+return-1;+}++/* Find the principal from the signers */+strvec_pushl(&ssh_keygen.args,fmt->program,"-Y","find-principals",+"-f",get_ssh_allowed_signers(),"-s",temp->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+)"));+returnret;+}+if(ret||!ssh_keygen_out.len){+/* We did not find a matching principal in the allowedSigners - Check+*withoutvalidation*/+child_process_init(&ssh_keygen);+strvec_pushl(&ssh_keygen.args,fmt->program,"-Y",+"check-novalidate","-n","git","-s",+temp->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",get_ssh_allowed_signers(),+"-I",principal,"-s",temp->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);++delete_tempfile(&temp);+strbuf_release(&ssh_keygen_out);+strbuf_release(&ssh_keygen_err);++returnret;+}+intcheck_signature(constchar*payload,size_tplen,constchar*signature,size_tslen,structsignature_check*sigc){
@@ -0,0 +1,398 @@+#!/bin/sh++test_description='ssh signed commit tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+GNUPGHOME_NOT_USED=$GNUPGHOME+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed commits''+test_oid_cache<<-\EOF&&+headersha1:gpgsig+headersha256:gpgsig-sha256+EOF++test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-S-minitial&&+gittaginitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-S-msecond&&+gittagsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittagmerge&&++echo4>file&&test_tick&&gitcommit-a-m"fourth unsigned"&&+gittagfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittagfourth-signed&&++gitconfigcommit.gpgsigntrue&&+echo5>file&&test_tick&&gitcommit-a-m"fifth signed"&&+gittagfifth-signed&&++gitconfigcommit.gpgsignfalse&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittagsixth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo7>file&&test_tick&&gitcommit-a-m"seventh"--no-gpg-sign&&+gittagseventh-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittagsixth-signedHEAD^&&+gittagseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth-S"${SIGNING_KEY_UNTRUSTED}"&&+gittageighth-signed-alt&&++# commit.gpgsign is still on but this must not be signed+echo9|gitcommit-treeHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagninth-unsigned$(catoid)&&+# explicit -S of course must sign.+echo10|gitcommit-tree-SHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtenth-signed$(catoid)&&++# --gpg-sign[=<key-id>] must sign.+echo11|gitcommit-tree--gpg-signHEAD^{tree}>oid&&+test_line_count=1oid&&+gittageleventh-signed$(catoid)&&+echo12|gitcommit-tree--gpg-sign="${SIGNING_KEY_UNTRUSTED}"HEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtwelfth-signed-alt$(catoid)+'++test_expect_successGPGSSH'verify and show signatures''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.mintrustlevelUNDEFINED&&+(+forcommitininitialsecondmergefourth-signed\+fifth-signedsixth-signedseventh-signedtenth-signed\+eleventh-signed+do+gitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsigned\+seventh-unsignedninth-unsigned+do+test_must_failgitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alttwelfth-signed-alt+do+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'verify-commit exits success on untrusted signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitverify-commiteighth-signed-alt2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual+'++test_expect_successGPGSSH'verify-commit exits success with matching minTrustLevel''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.minTrustLevelfully&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits success with low minTrustLevel''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.minTrustLevelmarginal&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits failure with high minTrustLevel''+test_configgpg.minTrustLevelultimate&&+test_must_failgitverify-commiteighth-signed-alt+'++test_expect_successGPGSSH'verify signatures with --raw''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+forcommitininitialsecondmergefourth-signedfifth-signedsixth-signedseventh-signed+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsignedseventh-unsigned+do+test_must_failgitverify-commit--raw$commit2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alt+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'proper header is used for hash algorithm''+gitcat-filecommitfourth-signed>output&&+grep"^$(test_oidheader) -----BEGIN SSH SIGNATURE-----"output+'++test_expect_successGPGSSH'show signed commit with signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitshow-sinitial>commit&&+gitshow-s--show-signatureinitial>show&&+gitverify-commit-vinitial>verify.12>verify.2&&+gitcat-filecommitinitial>cat&&+grep-v-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.commit&&+grep-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.gpg&&+grep-v"^ "cat|grep-v"^gpgsig.* ">cat.commit&&+test_cmpshow.commitcommit&&+test_cmpshow.gpgverify.2&&+test_cmpcat.commitverify.1+'++test_expect_successGPGSSH'detect fudged signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filecommitseventh-signed>raw&&+sed-e"s/^seventh/7th forged/"raw>forged1&&+githash-object-w-tcommitforged1>forged1.commit&&+test_must_failgitverify-commit$(catforged1.commit)&&+gitshow--pretty=short--show-signature$(catforged1.commit)>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'detect fudged signature with NUL''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filecommitseventh-signed>raw&&+catraw>forged2&&+echoQwik|tr"Q""\000">>forged2&&+githash-object-w-tcommitforged2>forged2.commit&&+test_must_failgitverify-commit$(catforged2.commit)&&+gitshow--pretty=short--show-signature$(catforged2.commit)>actual2&&+grep"${BAD_SIGNATURE}"actual2&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual2+'++test_expect_successGPGSSH'amending already signed commit''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcheckoutfourth-signed^0&&+gitcommit--amend-S--no-edit&&+gitverify-commitHEAD&&+gitshow-s--show-signatureHEAD>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual+'++test_expect_successGPGSSH'show good signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+cat>expect.tmpl<<-\EOF&&+G+FINGERPRINT+principalwithnumber1+FINGERPRINT++EOF+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show bad signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect<<-\EOF&&+B+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catforged1.commit)>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+U+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with undefined trust level''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+undefined+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with ultimate trust level''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+fully+FINGERPRINT+principalwithnumber1+FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show lack of signature with custom format''+cat>expect<<-\EOF&&+N+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"seventh-unsigned>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'log.showsignature behaves like --show-signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configlog.showsignaturetrue&&+gitshowinitial>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual+'++test_expect_successGPGSSH'check config gpg.format values''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.formatssh&&+gitcommit-S--amend-m"success"&&+test_configgpg.formatOpEnPgP&&+test_must_failgitcommit-S--amend-m"fail"+'++test_expect_failureGPGSSH'detect fudged commit with double signature (TODO)''+sed-e"/gpgsig/,/END PGP/d"forged1>double-base&&+sed-n-e"/gpgsig/,/END PGP/p"forged1|\+sed-e"s/^$(test_oidheader)//;s/^ //"|gpg--dearmor>double-sig1.sig&&+gpg-odouble-sig2.sig-u29472784--detach-signdouble-base&&+catdouble-sig1.sigdouble-sig2.sig|gpg--enarmor>double-combined.asc&&+sed-e"s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oidheader) /;2,\$s/^/ /"\+double-combined.asc>double-gpgsig&&+sed-e"/committer/r double-gpgsig"double-base>double-commit&&+githash-object-w-tcommitdouble-commit>double-commit.commit&&+test_must_failgitverify-commit$(catdouble-commit.commit)&&+gitshow--pretty=short--show-signature$(catdouble-commit.commit)>double-actual&&+grep"BAD signature from"double-actual&&+grep"Good signature from"double-actual+'++test_expect_failureGPGSSH'show double signature with custom format (TODO)''+cat>expect<<-\EOF&&+E+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catdouble-commit.commit)>actual&&+test_cmpexpectactual+'+++test_expect_failureGPGSSH'verify-commit verifies multiply signed commits (TODO)''+gitinitmultiply-signed&&+cdmultiply-signed&&+test_commitfirst&&+echo1>second&&+gitaddsecond&&+tree=$(gitwrite-tree)&&+parent=$(gitrev-parseHEAD^{commit})&&+gitcommit--gpg-sign-msecond&&+gitcat-filecommitHEAD&&+# Avoid trailing whitespace.+sed-e"s/^Q//"-e"s/^Z/ /">commit<<-EOF&&+Qtree$tree+Qparent$parent+QauthorAUThor<author@example.com>1112912653-0700+QcommitterCOMitter<committer@example.com>1112912653-0700+Qgpgsig-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC+QAKCTqBh+VabJceXcGIZuF0Ry+udbBQ==+Q=tQ0N+Q-----ENDPGPSIGNATURE-----+Qgpgsig-sha256-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO+QAJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==+Q=pIwP+Q-----ENDPGPSIGNATURE-----+Q+Qsecond+EOF+head=$(githash-object-tcommit-wcommit)&&+gitreset--hard$head&&+gitverify-commit$head2>actual&&+grep"Good signature from"actual&&+!grep"BAD signature from"actual+'++test_done
@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'ssh signed push sends push certificate''+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principalwithnumber1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'inconsistent push options in signed push not allowed''# First, invoke receive-pack with dummy input to obtain its preamble.prepare_dst&&
@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'fail without key and heed user.signingkey ssh''+test_configgpg.formatssh&&+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configuser.emailhasnokey@nowhere.com&&+test_configgpg.formatssh&&+test_configuser.signingkey""&&+(+sane_unsetGIT_COMMITTER_EMAIL&&+test_must_failgitpush--signeddstnoopff+noff+)&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principalwithnumber1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'failed atomic push does not execute GPG''prepare_dst&&git-Cdstconfigreceive.certnonceseedsekrit&&
@@ -0,0 +1,161 @@+#!/bin/sh++test_description='signed tag tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed tags ssh''+test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-minitial&&+gittag-s-minitialinitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-msecond&&+gittag-s-msecondsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittag-s-mmergemerge&&++echo4>file&&test_tick&&gitcommit-a-S-m"fourth unsigned"&&+gittag-a-mfourth-unsignedfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittag-s-mfourthfourth-signed&&++echo5>file&&test_tick&&gitcommit-a-m"fifth"&&+gittagfifth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittag-a-msixthsixth-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittag-s-m6thsixth-signedHEAD^&&+gittag-mseventh-sseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth&&+gittag-u"${SIGNING_KEY_UNTRUSTED}"-meightheighth-signed-alt+'++test_expect_successGPGSSH'verify and show ssh signatures''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'detect fudged ssh signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filetagseventh-signed>raw&&+sed-e"/^tag / s/seventh/7th forged/"raw>forged1&&+githash-object-w-ttagforged1>forged1.tag&&+test_must_failgitverify-tag$(catforged1.tag)2>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'verify ssh signatures with --raw''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag--raw$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'verify signatures with --raw ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitverify-tag--rawsixth-signed2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echosixth-signedOK+'++test_expect_successGPGSSH'verify multiple tags ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+tags="seventh-signed sixth-signed"&&+foriin$tags+do+gitverify-tag-v--raw$i||return1+done>expect.stdout2>expect.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<expect.stderr.1>expect.stderr&&+gitverify-tag-v--raw$tags>actual.stdout2>actual.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<actual.stderr.1>actual.stderr&&+test_cmpexpect.stdoutactual.stdout&&+test_cmpexpect.stderractual.stderr+'++test_expect_successGPGSSH'verifying tag with --format - ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect<<-\EOF&&+tagname:fourth-signed+EOF+gitverify-tag--format="tagname : %(tag)""fourth-signed">actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'verifying a forged tag with --format should fail silently - ssh''+test_must_failgitverify-tag--format="tagname : %(tag)"$(catforged1.tag)>actual-forged&&+test_must_be_emptyactual-forged+'++test_done
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -33,3 +33,38 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate`++gpg.ssh.allowedSignersFile::+ A file containing ssh public keys which you are willing to trust.+ The file consists of one or more lines of principals followed by an ssh+ public key.+ e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...+ See ssh-keygen(1) "ALLOWED SIGNERS" for details.+ The principal is only used to identify the key and is available when+ verifying a signature.+++SSH has no concept of trust levels like gpg does. To be able to differentiate+between valid signatures and trusted signatures the trust level of a signature+verification is set to `fully` when the public key is present in the allowedSignersFile.+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.+Otherwise valid but untrusted signatures will still verify but show no principal+name of the signer.+++This file can be set to a location outside of the repository and every developer+maintains their own trust store. A central repository server could generate this+file automatically from ssh keys with push access to verify the code against.+In a corporate setting this file is probably generated at a global location+from automation that already handles developer ssh keys.+++A repository that only allows signed commits can store the file+in the repository itself using a path relative to the top-level of the working tree.+This way only committers with an already valid key can add or change keys in the keyring.+++Using a SSH CA key with the cert-authority option+(see ssh-keygen(1) "CERTIFICATES") is also valid.++gpg.ssh.revocationFile::+ Either a SSH KRL or a list of revoked public keys (without the principal prefix).+ See ssh-keygen(1) for details.+ If a public key is found in this file then it will always be treated+ as having trust level "never" and signatures will show as invalid.
@@ -36,3 +36,9 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports.+ If gpg.format is set to "ssh" this can contain the literal ssh public+ key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and+ corresponds to the private key used for signing. The private key+ needs to be available via ssh-agent. Alternatively it can be set to+ a file containing a private key directly. If not set git will call+ "ssh-add -L" and try to use the first key available.
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-27 13:15:50
From: Fabian Stelzer <redacted>
Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.
In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.
To be able to implement new signing formats this commit:
- makes the sigc structure more generic by renaming "gpg_output" to
"output"
- introduces function pointers in the gpg_format structure to call
format specific signing and verification functions
- moves format detection from verify_signed_buffer into the check_signature
api function and calls the format specific verify
- renames and wraps sign_buffer to handle format specific signing logic
as well
Signed-off-by: Fabian Stelzer <redacted>
---
fmt-merge-msg.c | 6 +--
gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
gpg-interface.h | 2 +-
log-tree.c | 8 ++--
pretty.c | 4 +-
5 files changed, 74 insertions(+), 50 deletions(-)
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,"--verify",temp->filename.buf,"-",NULL);-if(!gpg_status)-gpg_status=&buf;-sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,payload,payload_size,-gpg_status,0,gpg_output,0);+ret=pipe_command(&gpg,payload,payload_size,&gpg_stdout,0,+&gpg_stderr,0);sigchain_pop(SIGPIPE);delete_tempfile(&temp);-ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");-strbuf_release(&buf);/* no matter it was used or not */+ret|=!strstr(gpg_stdout.buf,"\n[GNUPG:] GOODSIG ");+sigc->payload=xmemdupz(payload,payload_size);+sigc->output=strbuf_detach(&gpg_stderr,NULL);+sigc->gpg_status=strbuf_detach(&gpg_stdout,NULL);++parse_gpg_output(sigc);++strbuf_release(&gpg_stdout);+strbuf_release(&gpg_stderr);returnret;}
@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,/* could have a good signature */status=check_signature(payload.buf,payload.len,signature.buf,signature.len,&sigc);-if(sigc.gpg_output)-strbuf_addstr(&verify_message,sigc.gpg_output);+if(sigc.output)+strbuf_addstr(&verify_message,sigc.output);elsestrbuf_addstr(&verify_message,"No signature\n");signature_check_clear(&sigc);
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-27 13:15:53
From: Fabian Stelzer <redacted>
implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.
gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 129 insertions(+), 8 deletions(-)
@@ -494,13 +531,97 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,returnerror(_("gpg failed to sign the data"));/* Strip CR from the line endings, in case we are on Windows. */-for(i=j=bottom;i<signature->len;i++)-if(signature->buf[i]!='\r'){-if(i!=j)-signature->buf[j]=signature->buf[i];-j++;-}-strbuf_setlen(signature,j);+remove_cr_after(signature,bottom);return0;}++staticintsign_buffer_ssh(structstrbuf*buffer,structstrbuf*signature,+constchar*signing_key)+{+structchild_processsigner=CHILD_PROCESS_INIT;+intret=-1;+size_tbottom,keylen;+structstrbufsigner_stderr=STRBUF_INIT;+structtempfile*key_file=NULL,*buffer_file=NULL;+char*ssh_signing_key_file=NULL;+structstrbufssh_signature_filename=STRBUF_INIT;++if(!signing_key||signing_key[0]=='\0')+returnerror(+_("user.signingkey needs to be set for ssh signing"));++if(starts_with(signing_key,"ssh-")){+/* A literal ssh key */+key_file=mks_tempfile_t(".git_signing_key_tmpXXXXXX");+if(!key_file)+returnerror_errno(+_("could not create temporary file"));+keylen=strlen(signing_key);+if(write_in_full(key_file->fd,signing_key,keylen)<0||+close_tempfile_gently(key_file)<0){+error_errno(_("failed writing ssh signing key to '%s'"),+key_file->filename.buf);+gotoout;+}+ssh_signing_key_file=key_file->filename.buf;+}else{+/* We assume a file */+ssh_signing_key_file=expand_user_path(signing_key,1);+}++buffer_file=mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");+if(!buffer_file){+error_errno(_("could not create temporary file"));+gotoout;+}++if(write_in_full(buffer_file->fd,buffer->buf,buffer->len)<0||+close_tempfile_gently(buffer_file)<0){+error_errno(_("failed writing ssh signing key buffer to '%s'"),+buffer_file->filename.buf);+gotoout;+}++strvec_pushl(&signer.args,use_format->program,+"-Y","sign",+"-n","git",+"-f",ssh_signing_key_file,+buffer_file->filename.buf,+NULL);++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&signer,NULL,0,NULL,0,&signer_stderr,0);+sigchain_pop(SIGPIPE);++if(ret){+if(strstr(signer_stderr.buf,"usage:"))+error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));++error("%s",signer_stderr.buf);+gotoout;+}++bottom=signature->len;++strbuf_addbuf(&ssh_signature_filename,&buffer_file->filename);+strbuf_addstr(&ssh_signature_filename,".sig");+if(strbuf_read_file(signature,ssh_signature_filename.buf,0)<0){+error_errno(+_("failed reading ssh signing data buffer from '%s'"),+ssh_signature_filename.buf);+}+unlink_or_warn(ssh_signature_filename.buf);++/* Strip CR from the line endings, in case we are on Windows. */+remove_cr_after(signature,bottom);++out:+if(key_file)+delete_tempfile(&key_file);+if(buffer_file)+delete_tempfile(&buffer_file);+strbuf_release(&signer_stderr);+strbuf_release(&ssh_signature_filename);+returnret;+}
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-27 13:15:57
From: Fabian Stelzer <redacted>
for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.
in push certs and textual output we prefer the ssh fingerprint instead.
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
gpg-interface.h | 6 ++++++
send-pack.c | 8 ++++----
3 files changed, 56 insertions(+), 4 deletions(-)
@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+staticchar*get_ssh_key_fingerprint(constchar*signing_key)+{+structchild_processssh_keygen=CHILD_PROCESS_INIT;+intret=-1;+structstrbuffingerprint_stdout=STRBUF_INIT;+structstrbuf**fingerprint;++/*+*WithSSHSigningthiscancontainafilenameorapublickey+*Fortextualrepresentationweusuallywantafingerprint+*/+if(istarts_with(signing_key,"ssh-")){+strvec_pushl(&ssh_keygen.args,"ssh-keygen","-lf","-",NULL);+ret=pipe_command(&ssh_keygen,signing_key,+strlen(signing_key),&fingerprint_stdout,0,+NULL,0);+}else{+strvec_pushl(&ssh_keygen.args,"ssh-keygen","-lf",+configured_signing_key,NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&fingerprint_stdout,0,+NULL,0);+}++if(!!ret)+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++fingerprint=strbuf_split_max(&fingerprint_stdout,' ',3);+if(!fingerprint[1])+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++returnstrbuf_detach(fingerprint[1],NULL);+}+/* Returns the first public key from an ssh-agent to use for signing */staticchar*get_default_ssh_signing_key(void){
@@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)return"";}+/* Returns a textual but unique representation ot the signing key */+constchar*get_signing_key_id(void)+{+if(!strcmp(use_format->name,"ssh")){+returnget_ssh_key_fingerprint(get_signing_key());+}else{+/* GPG/GPGSM only store a key id on this variable */+returnget_signing_key();+}+}+constchar*get_signing_key(void){if(configured_signing_key)
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,{conststructref*ref;structstring_list_item*item;-char*signing_key=xstrdup(get_signing_key());+char*signing_key_id=xstrdup(get_signing_key_id());constchar*cp,*np;structstrbufcert=STRBUF_INIT;intupdate_seen=0;strbuf_addstr(&cert,"certificate version 0.1\n");-strbuf_addf(&cert,"pusher %s ",signing_key);+strbuf_addf(&cert,"pusher %s ",signing_key_id);datestamp(&cert);strbuf_addch(&cert,'\n');if(args->url&&*args->url){
@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,if(!update_seen)gotofree_return;-if(sign_buffer(&cert,&cert,signing_key))+if(sign_buffer(&cert,&cert,get_signing_key()))die(_("failed to sign the push certificate"));packet_buf_write(req_buf,"push-cert%c%s",0,cap_string);
@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,packet_buf_write(req_buf,"push-cert-end\n");free_return:-free(signing_key);+free(signing_key_id);strbuf_release(&cert);returnupdate_seen;}
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-27 13:15:58
From: Fabian Stelzer <redacted>
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+/* Returns the first public key from an ssh-agent to use for signing */+staticchar*get_default_ssh_signing_key(void)+{+structchild_processssh_add=CHILD_PROCESS_INIT;+intret=-1;+structstrbufkey_stdout=STRBUF_INIT;+structstrbuf**keys;++strvec_pushl(&ssh_add.args,"ssh-add","-L",NULL);+ret=pipe_command(&ssh_add,NULL,0,&key_stdout,0,NULL,0);+if(!ret){+keys=strbuf_split_max(&key_stdout,'\n',2);+if(keys[0])+returnstrbuf_detach(keys[0],NULL);+}++strbuf_release(&key_stdout);+return"";+}+constchar*get_signing_key(void){if(configured_signing_key)returnconfigured_signing_key;-returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+if(!strcmp(use_format->name,"ssh")){+returnget_default_ssh_signing_key();+}else{+returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+}}intsign_buffer(structstrbuf*buffer,structstrbuf*signature,constchar*signing_key)
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-27 13:16:00
From: Fabian Stelzer <redacted>
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.
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 <redacted>
---
builtin/receive-pack.c | 2 +
gpg-interface.c | 179 ++++++++++++++++++++++++++++++++++++++++-
2 files changed, 180 insertions(+), 1 deletion(-)
@@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,returnret;}+staticvoidparse_ssh_output(structsignature_check*sigc)+{+constchar*line,*principal,*search;++/*+*ssh-keysignoutputshouldbe:+*Good"git"signatureforPRINCIPALwithRSAkeySHA256:FINGERPRINT+*Good"git"signatureforPRINCIPALWITHWHITESPACEwithRSAkeySHA256:FINGERPRINT+*orforvalidbutunknownkeys:+*Good"git"signaturewithRSAkeySHA256: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);+}elseif(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);+}+}++staticintverify_ssh_signed_buffer(structsignature_check*sigc,+structgpg_format*fmt,constchar*payload,+size_tpayload_size,constchar*signature,+size_tsignature_size)+{+structchild_processssh_keygen=CHILD_PROCESS_INIT;+structtempfile*buffer_file;+intret=-1;+constchar*line;+size_ttrust_size;+char*principal;+structstrbufssh_keygen_out=STRBUF_INIT;+structstrbufssh_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)+returnerror_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+)"));+gotoout;+}+if(ret||!ssh_keygen_out.len){+/* We did not find a matching principal in the allowedSigners - Check+*withoutvalidation*/+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);++returnret;+}+intcheck_signature(constchar*payload,size_tplen,constchar*signature,size_tslen,structsignature_check*sigc){
@@ -0,0 +1,398 @@+#!/bin/sh++test_description='ssh signed commit tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+GNUPGHOME_NOT_USED=$GNUPGHOME+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed commits''+test_oid_cache<<-\EOF&&+headersha1:gpgsig+headersha256:gpgsig-sha256+EOF++test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-S-minitial&&+gittaginitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-S-msecond&&+gittagsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittagmerge&&++echo4>file&&test_tick&&gitcommit-a-m"fourth unsigned"&&+gittagfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittagfourth-signed&&++gitconfigcommit.gpgsigntrue&&+echo5>file&&test_tick&&gitcommit-a-m"fifth signed"&&+gittagfifth-signed&&++gitconfigcommit.gpgsignfalse&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittagsixth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo7>file&&test_tick&&gitcommit-a-m"seventh"--no-gpg-sign&&+gittagseventh-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittagsixth-signedHEAD^&&+gittagseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth-S"${SIGNING_KEY_UNTRUSTED}"&&+gittageighth-signed-alt&&++# commit.gpgsign is still on but this must not be signed+echo9|gitcommit-treeHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagninth-unsigned$(catoid)&&+# explicit -S of course must sign.+echo10|gitcommit-tree-SHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtenth-signed$(catoid)&&++# --gpg-sign[=<key-id>] must sign.+echo11|gitcommit-tree--gpg-signHEAD^{tree}>oid&&+test_line_count=1oid&&+gittageleventh-signed$(catoid)&&+echo12|gitcommit-tree--gpg-sign="${SIGNING_KEY_UNTRUSTED}"HEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtwelfth-signed-alt$(catoid)+'++test_expect_successGPGSSH'verify and show signatures''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.mintrustlevelUNDEFINED&&+(+forcommitininitialsecondmergefourth-signed\+fifth-signedsixth-signedseventh-signedtenth-signed\+eleventh-signed+do+gitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsigned\+seventh-unsignedninth-unsigned+do+test_must_failgitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alttwelfth-signed-alt+do+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'verify-commit exits success on untrusted signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitverify-commiteighth-signed-alt2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual+'++test_expect_successGPGSSH'verify-commit exits success with matching minTrustLevel''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.minTrustLevelfully&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits success with low minTrustLevel''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.minTrustLevelmarginal&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits failure with high minTrustLevel''+test_configgpg.minTrustLevelultimate&&+test_must_failgitverify-commiteighth-signed-alt+'++test_expect_successGPGSSH'verify signatures with --raw''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+forcommitininitialsecondmergefourth-signedfifth-signedsixth-signedseventh-signed+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsignedseventh-unsigned+do+test_must_failgitverify-commit--raw$commit2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alt+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'proper header is used for hash algorithm''+gitcat-filecommitfourth-signed>output&&+grep"^$(test_oidheader) -----BEGIN SSH SIGNATURE-----"output+'++test_expect_successGPGSSH'show signed commit with signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitshow-sinitial>commit&&+gitshow-s--show-signatureinitial>show&&+gitverify-commit-vinitial>verify.12>verify.2&&+gitcat-filecommitinitial>cat&&+grep-v-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.commit&&+grep-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.gpg&&+grep-v"^ "cat|grep-v"^gpgsig.* ">cat.commit&&+test_cmpshow.commitcommit&&+test_cmpshow.gpgverify.2&&+test_cmpcat.commitverify.1+'++test_expect_successGPGSSH'detect fudged signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filecommitseventh-signed>raw&&+sed-e"s/^seventh/7th forged/"raw>forged1&&+githash-object-w-tcommitforged1>forged1.commit&&+test_must_failgitverify-commit$(catforged1.commit)&&+gitshow--pretty=short--show-signature$(catforged1.commit)>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'detect fudged signature with NUL''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filecommitseventh-signed>raw&&+catraw>forged2&&+echoQwik|tr"Q""\000">>forged2&&+githash-object-w-tcommitforged2>forged2.commit&&+test_must_failgitverify-commit$(catforged2.commit)&&+gitshow--pretty=short--show-signature$(catforged2.commit)>actual2&&+grep"${BAD_SIGNATURE}"actual2&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual2+'++test_expect_successGPGSSH'amending already signed commit''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcheckoutfourth-signed^0&&+gitcommit--amend-S--no-edit&&+gitverify-commitHEAD&&+gitshow-s--show-signatureHEAD>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual+'++test_expect_successGPGSSH'show good signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+cat>expect.tmpl<<-\EOF&&+G+FINGERPRINT+principalwithnumber1+FINGERPRINT++EOF+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show bad signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect<<-\EOF&&+B+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catforged1.commit)>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+U+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with undefined trust level''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+undefined+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with ultimate trust level''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+fully+FINGERPRINT+principalwithnumber1+FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show lack of signature with custom format''+cat>expect<<-\EOF&&+N+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"seventh-unsigned>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'log.showsignature behaves like --show-signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configlog.showsignaturetrue&&+gitshowinitial>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual+'++test_expect_successGPGSSH'check config gpg.format values''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.formatssh&&+gitcommit-S--amend-m"success"&&+test_configgpg.formatOpEnPgP&&+test_must_failgitcommit-S--amend-m"fail"+'++test_expect_failureGPGSSH'detect fudged commit with double signature (TODO)''+sed-e"/gpgsig/,/END PGP/d"forged1>double-base&&+sed-n-e"/gpgsig/,/END PGP/p"forged1|\+sed-e"s/^$(test_oidheader)//;s/^ //"|gpg--dearmor>double-sig1.sig&&+gpg-odouble-sig2.sig-u29472784--detach-signdouble-base&&+catdouble-sig1.sigdouble-sig2.sig|gpg--enarmor>double-combined.asc&&+sed-e"s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oidheader) /;2,\$s/^/ /"\+double-combined.asc>double-gpgsig&&+sed-e"/committer/r double-gpgsig"double-base>double-commit&&+githash-object-w-tcommitdouble-commit>double-commit.commit&&+test_must_failgitverify-commit$(catdouble-commit.commit)&&+gitshow--pretty=short--show-signature$(catdouble-commit.commit)>double-actual&&+grep"BAD signature from"double-actual&&+grep"Good signature from"double-actual+'++test_expect_failureGPGSSH'show double signature with custom format (TODO)''+cat>expect<<-\EOF&&+E+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catdouble-commit.commit)>actual&&+test_cmpexpectactual+'+++test_expect_failureGPGSSH'verify-commit verifies multiply signed commits (TODO)''+gitinitmultiply-signed&&+cdmultiply-signed&&+test_commitfirst&&+echo1>second&&+gitaddsecond&&+tree=$(gitwrite-tree)&&+parent=$(gitrev-parseHEAD^{commit})&&+gitcommit--gpg-sign-msecond&&+gitcat-filecommitHEAD&&+# Avoid trailing whitespace.+sed-e"s/^Q//"-e"s/^Z/ /">commit<<-EOF&&+Qtree$tree+Qparent$parent+QauthorAUThor<author@example.com>1112912653-0700+QcommitterCOMitter<committer@example.com>1112912653-0700+Qgpgsig-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC+QAKCTqBh+VabJceXcGIZuF0Ry+udbBQ==+Q=tQ0N+Q-----ENDPGPSIGNATURE-----+Qgpgsig-sha256-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO+QAJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==+Q=pIwP+Q-----ENDPGPSIGNATURE-----+Q+Qsecond+EOF+head=$(githash-object-tcommit-wcommit)&&+gitreset--hard$head&&+gitverify-commit$head2>actual&&+grep"Good signature from"actual&&+!grep"BAD signature from"actual+'++test_done
@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'ssh signed push sends push certificate''+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principalwithnumber1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'inconsistent push options in signed push not allowed''# First, invoke receive-pack with dummy input to obtain its preamble.prepare_dst&&
@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'fail without key and heed user.signingkey ssh''+test_configgpg.formatssh&&+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configuser.emailhasnokey@nowhere.com&&+test_configgpg.formatssh&&+test_configuser.signingkey""&&+(+sane_unsetGIT_COMMITTER_EMAIL&&+test_must_failgitpush--signeddstnoopff+noff+)&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principalwithnumber1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'failed atomic push does not execute GPG''prepare_dst&&git-Cdstconfigreceive.certnonceseedsekrit&&
@@ -0,0 +1,161 @@+#!/bin/sh++test_description='signed tag tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed tags ssh''+test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-minitial&&+gittag-s-minitialinitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-msecond&&+gittag-s-msecondsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittag-s-mmergemerge&&++echo4>file&&test_tick&&gitcommit-a-S-m"fourth unsigned"&&+gittag-a-mfourth-unsignedfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittag-s-mfourthfourth-signed&&++echo5>file&&test_tick&&gitcommit-a-m"fifth"&&+gittagfifth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittag-a-msixthsixth-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittag-s-m6thsixth-signedHEAD^&&+gittag-mseventh-sseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth&&+gittag-u"${SIGNING_KEY_UNTRUSTED}"-meightheighth-signed-alt+'++test_expect_successGPGSSH'verify and show ssh signatures''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'detect fudged ssh signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filetagseventh-signed>raw&&+sed-e"/^tag / s/seventh/7th forged/"raw>forged1&&+githash-object-w-ttagforged1>forged1.tag&&+test_must_failgitverify-tag$(catforged1.tag)2>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'verify ssh signatures with --raw''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag--raw$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'verify signatures with --raw ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitverify-tag--rawsixth-signed2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echosixth-signedOK+'++test_expect_successGPGSSH'verify multiple tags ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+tags="seventh-signed sixth-signed"&&+foriin$tags+do+gitverify-tag-v--raw$i||return1+done>expect.stdout2>expect.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<expect.stderr.1>expect.stderr&&+gitverify-tag-v--raw$tags>actual.stdout2>actual.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<actual.stderr.1>actual.stderr&&+test_cmpexpect.stdoutactual.stdout&&+test_cmpexpect.stderractual.stderr+'++test_expect_successGPGSSH'verifying tag with --format - ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect<<-\EOF&&+tagname:fourth-signed+EOF+gitverify-tag--format="tagname : %(tag)""fourth-signed">actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'verifying a forged tag with --format should fail silently - ssh''+test_must_failgitverify-tag--format="tagname : %(tag)"$(catforged1.tag)>actual-forged&&+test_must_be_emptyactual-forged+'++test_done
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -33,3 +33,38 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate`++gpg.ssh.allowedSignersFile::+ A file containing ssh public keys which you are willing to trust.+ The file consists of one or more lines of principals followed by an ssh+ public key.+ e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...+ See ssh-keygen(1) "ALLOWED SIGNERS" for details.+ The principal is only used to identify the key and is available when+ verifying a signature.+++SSH has no concept of trust levels like gpg does. To be able to differentiate+between valid signatures and trusted signatures the trust level of a signature+verification is set to `fully` when the public key is present in the allowedSignersFile.+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.+Otherwise valid but untrusted signatures will still verify but show no principal+name of the signer.+++This file can be set to a location outside of the repository and every developer+maintains their own trust store. A central repository server could generate this+file automatically from ssh keys with push access to verify the code against.+In a corporate setting this file is probably generated at a global location+from automation that already handles developer ssh keys.+++A repository that only allows signed commits can store the file+in the repository itself using a path relative to the top-level of the working tree.+This way only committers with an already valid key can add or change keys in the keyring.+++Using a SSH CA key with the cert-authority option+(see ssh-keygen(1) "CERTIFICATES") is also valid.++gpg.ssh.revocationFile::+ Either a SSH KRL or a list of revoked public keys (without the principal prefix).+ See ssh-keygen(1) for details.+ If a public key is found in this file then it will always be treated+ as having trust level "never" and signatures will show as invalid.
@@ -36,3 +36,9 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports.+ If gpg.format is set to "ssh" this can contain the literal ssh public+ key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and+ corresponds to the private key used for signing. The private key+ needs to be available via ssh-agent. Alternatively it can be set to+ a file containing a private key directly. If not set git will call+ "ssh-add -L" and try to use the first key available.
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-28 19:36:54
I have added support for using keyfiles directly, lots of tests and
generally cleaned up the signing & verification code a lot.
I can still rename things from being gpg specific to a more general
"signing" but thats rather cosmetic. Also i'm not sure if i named the new
test files correctly.
openssh 8.7 will add valid-after, valid-before options to the allowed keys
keyring. This allows us to pass the commit timestamp to the verification
call and make key rollover possible and still be able to verify older
commits. Set valid-after=NOW when adding your key to the keyring and set
valid-before to make it fail if used after a certain date. Software like
gitolite/github or corporate automation can do this automatically when ssh
push keys are addded / removed I will add this feature in a follow up patch
afterwards.
v3 addresses some issues & refactoring and splits the large commit into
several smaller ones.
v4:
* restructures and cleans up the whole patch set - patches build on its own
now and commit messages try to explain whats going on
* got rid of the if branches and used callback functions in the format
struct
* fixed a bug with whitespace in principal identifiers that required a
rewrite of the parse_ssh_output function
* rewrote documentation to be more clear - also renamed keyring back to
allowedSignersFile
v5:
* moved t7527 to t7528 to not collide with another patch in "seen"
* clean up return logic for failed signing & verification
* some minor renames / reformatting to make things clearer
v6: fixed tests when using shm output dir
Fabian Stelzer (9):
ssh signing: preliminary refactoring and clean-up
ssh signing: add ssh signature format and signing using ssh keys
ssh signing: retrieve a default key from ssh-agent
ssh signing: provide a textual representation of the signing key
ssh signing: parse ssh-keygen output and verify signatures
ssh signing: add test prereqs
ssh signing: duplicate t7510 tests for commits
ssh signing: add more tests for logs, tags & push certs
ssh signing: add documentation
Documentation/config/gpg.txt | 39 ++-
Documentation/config/user.txt | 6 +
builtin/receive-pack.c | 2 +
fmt-merge-msg.c | 6 +-
gpg-interface.c | 490 +++++++++++++++++++++++++++----
gpg-interface.h | 8 +-
log-tree.c | 8 +-
pretty.c | 4 +-
send-pack.c | 8 +-
t/lib-gpg.sh | 29 ++
t/t4202-log.sh | 23 ++
t/t5534-push-signed.sh | 101 +++++++
t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++
t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++
14 files changed, 1218 insertions(+), 65 deletions(-)
create mode 100755 t/t7031-verify-tag-signed-ssh.sh
create mode 100755 t/t7528-signed-commit-ssh.sh
base-commit: eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v6
Pull-Request: https://github.com/git/git/pull/1041
Range-diff vs v5:
1: 7c8502c65b8 = 1: 7c8502c65b8 ssh signing: preliminary refactoring and clean-up
2: f05bab16096 = 2: f05bab16096 ssh signing: add ssh signature format and signing using ssh keys
3: 071e6173d8e = 3: 071e6173d8e ssh signing: retrieve a default key from ssh-agent
4: 7d1d131ff5b = 4: 7d1d131ff5b ssh signing: provide a textual representation of the signing key
5: 725764018ce = 5: 725764018ce ssh signing: parse ssh-keygen output and verify signatures
6: eb677b1b6a8 ! 6: 18a26ca49e7 ssh signing: add test prereqs
@@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
+ test $? = 0 || exit 1;
+ mkdir -p "${GNUPGHOME}" &&
+ chmod 0700 "${GNUPGHOME}" &&
-+ ssh-keygen -t ed25519 -N "" -f "${GNUPGHOME}/ed25519_ssh_signing_key" >/dev/null &&
-+ ssh-keygen -t rsa -b 2048 -N "" -f "${GNUPGHOME}/rsa_2048_ssh_signing_key" >/dev/null &&
-+ ssh-keygen -t ed25519 -N "super_secret" -f "${GNUPGHOME}/protected_ssh_signing_key" >/dev/null &&
-+ find "${GNUPGHOME}" -name *ssh_signing_key.pub -exec cat {} \; | awk "{print \"\\\"principal with number \" NR \"\\\" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
++ ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GNUPGHOME}/ed25519_ssh_signing_key" >/dev/null &&
++ echo "\"principal with number 1\" $(cat "${GNUPGHOME}/ed25519_ssh_signing_key.pub")" >> "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
++ ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GNUPGHOME}/rsa_2048_ssh_signing_key" >/dev/null &&
++ echo "\"principal with number 2\" $(cat "${GNUPGHOME}/rsa_2048_ssh_signing_key.pub")" >> "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
++ ssh-keygen -t ed25519 -N "super_secret" -C "git ed25519 encrypted key" -f "${GNUPGHOME}/protected_ssh_signing_key" >/dev/null &&
++ echo "\"principal with number 3\" $(cat "${GNUPGHOME}/protected_ssh_signing_key.pub")" >> "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
+ cat "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
+ ssh-keygen -t ed25519 -N "" -f "${GNUPGHOME}/untrusted_ssh_signing_key" >/dev/null
+'
7: c877951df23 = 7: 01da9a07934 ssh signing: duplicate t7510 tests for commits
8: 60265e8c399 = 8: d9707443f5c ssh signing: add more tests for logs, tags & push certs
9: f758ce0ade4 = 9: 275af516eba ssh signing: add documentation
--
gitgitgadget
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-28 19:36:56
From: Fabian Stelzer <redacted>
Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.
In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.
To be able to implement new signing formats this commit:
- makes the sigc structure more generic by renaming "gpg_output" to
"output"
- introduces function pointers in the gpg_format structure to call
format specific signing and verification functions
- moves format detection from verify_signed_buffer into the check_signature
api function and calls the format specific verify
- renames and wraps sign_buffer to handle format specific signing logic
as well
Signed-off-by: Fabian Stelzer <redacted>
---
fmt-merge-msg.c | 6 +--
gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
gpg-interface.h | 2 +-
log-tree.c | 8 ++--
pretty.c | 4 +-
5 files changed, 74 insertions(+), 50 deletions(-)
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,"--verify",temp->filename.buf,"-",NULL);-if(!gpg_status)-gpg_status=&buf;-sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,payload,payload_size,-gpg_status,0,gpg_output,0);+ret=pipe_command(&gpg,payload,payload_size,&gpg_stdout,0,+&gpg_stderr,0);sigchain_pop(SIGPIPE);delete_tempfile(&temp);-ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");-strbuf_release(&buf);/* no matter it was used or not */+ret|=!strstr(gpg_stdout.buf,"\n[GNUPG:] GOODSIG ");+sigc->payload=xmemdupz(payload,payload_size);+sigc->output=strbuf_detach(&gpg_stderr,NULL);+sigc->gpg_status=strbuf_detach(&gpg_stdout,NULL);++parse_gpg_output(sigc);++strbuf_release(&gpg_stdout);+strbuf_release(&gpg_stderr);returnret;}
@@ -583,8 +583,8 @@ static int show_one_mergetag(struct commit *commit,/* could have a good signature */status=check_signature(payload.buf,payload.len,signature.buf,signature.len,&sigc);-if(sigc.gpg_output)-strbuf_addstr(&verify_message,sigc.gpg_output);+if(sigc.output)+strbuf_addstr(&verify_message,sigc.output);elsestrbuf_addstr(&verify_message,"No signature\n");signature_check_clear(&sigc);
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-28 19:36:58
From: Fabian Stelzer <redacted>
implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.
gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 129 insertions(+), 8 deletions(-)
@@ -494,13 +531,97 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,returnerror(_("gpg failed to sign the data"));/* Strip CR from the line endings, in case we are on Windows. */-for(i=j=bottom;i<signature->len;i++)-if(signature->buf[i]!='\r'){-if(i!=j)-signature->buf[j]=signature->buf[i];-j++;-}-strbuf_setlen(signature,j);+remove_cr_after(signature,bottom);return0;}++staticintsign_buffer_ssh(structstrbuf*buffer,structstrbuf*signature,+constchar*signing_key)+{+structchild_processsigner=CHILD_PROCESS_INIT;+intret=-1;+size_tbottom,keylen;+structstrbufsigner_stderr=STRBUF_INIT;+structtempfile*key_file=NULL,*buffer_file=NULL;+char*ssh_signing_key_file=NULL;+structstrbufssh_signature_filename=STRBUF_INIT;++if(!signing_key||signing_key[0]=='\0')+returnerror(+_("user.signingkey needs to be set for ssh signing"));++if(starts_with(signing_key,"ssh-")){+/* A literal ssh key */+key_file=mks_tempfile_t(".git_signing_key_tmpXXXXXX");+if(!key_file)+returnerror_errno(+_("could not create temporary file"));+keylen=strlen(signing_key);+if(write_in_full(key_file->fd,signing_key,keylen)<0||+close_tempfile_gently(key_file)<0){+error_errno(_("failed writing ssh signing key to '%s'"),+key_file->filename.buf);+gotoout;+}+ssh_signing_key_file=key_file->filename.buf;+}else{+/* We assume a file */+ssh_signing_key_file=expand_user_path(signing_key,1);+}++buffer_file=mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");+if(!buffer_file){+error_errno(_("could not create temporary file"));+gotoout;+}++if(write_in_full(buffer_file->fd,buffer->buf,buffer->len)<0||+close_tempfile_gently(buffer_file)<0){+error_errno(_("failed writing ssh signing key buffer to '%s'"),+buffer_file->filename.buf);+gotoout;+}++strvec_pushl(&signer.args,use_format->program,+"-Y","sign",+"-n","git",+"-f",ssh_signing_key_file,+buffer_file->filename.buf,+NULL);++sigchain_push(SIGPIPE,SIG_IGN);+ret=pipe_command(&signer,NULL,0,NULL,0,&signer_stderr,0);+sigchain_pop(SIGPIPE);++if(ret){+if(strstr(signer_stderr.buf,"usage:"))+error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));++error("%s",signer_stderr.buf);+gotoout;+}++bottom=signature->len;++strbuf_addbuf(&ssh_signature_filename,&buffer_file->filename);+strbuf_addstr(&ssh_signature_filename,".sig");+if(strbuf_read_file(signature,ssh_signature_filename.buf,0)<0){+error_errno(+_("failed reading ssh signing data buffer from '%s'"),+ssh_signature_filename.buf);+}+unlink_or_warn(ssh_signature_filename.buf);++/* Strip CR from the line endings, in case we are on Windows. */+remove_cr_after(signature,bottom);++out:+if(key_file)+delete_tempfile(&key_file);+if(buffer_file)+delete_tempfile(&buffer_file);+strbuf_release(&signer_stderr);+strbuf_release(&ssh_signature_filename);+returnret;+}
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-28 19:36:59
From: Fabian Stelzer <redacted>
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+/* Returns the first public key from an ssh-agent to use for signing */+staticchar*get_default_ssh_signing_key(void)+{+structchild_processssh_add=CHILD_PROCESS_INIT;+intret=-1;+structstrbufkey_stdout=STRBUF_INIT;+structstrbuf**keys;++strvec_pushl(&ssh_add.args,"ssh-add","-L",NULL);+ret=pipe_command(&ssh_add,NULL,0,&key_stdout,0,NULL,0);+if(!ret){+keys=strbuf_split_max(&key_stdout,'\n',2);+if(keys[0])+returnstrbuf_detach(keys[0],NULL);+}++strbuf_release(&key_stdout);+return"";+}+constchar*get_signing_key(void){if(configured_signing_key)returnconfigured_signing_key;-returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+if(!strcmp(use_format->name,"ssh")){+returnget_default_ssh_signing_key();+}else{+returngit_committer_info(IDENT_STRICT|IDENT_NO_DATE);+}}intsign_buffer(structstrbuf*buffer,structstrbuf*signature,constchar*signing_key)
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-28 19:37:00
From: Fabian Stelzer <redacted>
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.
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 <redacted>
---
builtin/receive-pack.c | 2 +
gpg-interface.c | 179 ++++++++++++++++++++++++++++++++++++++++-
2 files changed, 180 insertions(+), 1 deletion(-)
@@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,returnret;}+staticvoidparse_ssh_output(structsignature_check*sigc)+{+constchar*line,*principal,*search;++/*+*ssh-keysignoutputshouldbe:+*Good"git"signatureforPRINCIPALwithRSAkeySHA256:FINGERPRINT+*Good"git"signatureforPRINCIPALWITHWHITESPACEwithRSAkeySHA256:FINGERPRINT+*orforvalidbutunknownkeys:+*Good"git"signaturewithRSAkeySHA256: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);+}elseif(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);+}+}++staticintverify_ssh_signed_buffer(structsignature_check*sigc,+structgpg_format*fmt,constchar*payload,+size_tpayload_size,constchar*signature,+size_tsignature_size)+{+structchild_processssh_keygen=CHILD_PROCESS_INIT;+structtempfile*buffer_file;+intret=-1;+constchar*line;+size_ttrust_size;+char*principal;+structstrbufssh_keygen_out=STRBUF_INIT;+structstrbufssh_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)+returnerror_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+)"));+gotoout;+}+if(ret||!ssh_keygen_out.len){+/* We did not find a matching principal in the allowedSigners - Check+*withoutvalidation*/+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);++returnret;+}+intcheck_signature(constchar*payload,size_tplen,constchar*signature,size_tslen,structsignature_check*sigc){
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-07-28 19:37:03
From: Fabian Stelzer <redacted>
for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.
in push certs and textual output we prefer the ssh fingerprint instead.
Signed-off-by: Fabian Stelzer <redacted>
---
gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
gpg-interface.h | 6 ++++++
send-pack.c | 8 ++++----
3 files changed, 56 insertions(+), 4 deletions(-)
@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)return0;}+staticchar*get_ssh_key_fingerprint(constchar*signing_key)+{+structchild_processssh_keygen=CHILD_PROCESS_INIT;+intret=-1;+structstrbuffingerprint_stdout=STRBUF_INIT;+structstrbuf**fingerprint;++/*+*WithSSHSigningthiscancontainafilenameorapublickey+*Fortextualrepresentationweusuallywantafingerprint+*/+if(istarts_with(signing_key,"ssh-")){+strvec_pushl(&ssh_keygen.args,"ssh-keygen","-lf","-",NULL);+ret=pipe_command(&ssh_keygen,signing_key,+strlen(signing_key),&fingerprint_stdout,0,+NULL,0);+}else{+strvec_pushl(&ssh_keygen.args,"ssh-keygen","-lf",+configured_signing_key,NULL);+ret=pipe_command(&ssh_keygen,NULL,0,&fingerprint_stdout,0,+NULL,0);+}++if(!!ret)+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++fingerprint=strbuf_split_max(&fingerprint_stdout,' ',3);+if(!fingerprint[1])+die_errno(_("failed to get the ssh fingerprint for key '%s'"),+signing_key);++returnstrbuf_detach(fingerprint[1],NULL);+}+/* Returns the first public key from an ssh-agent to use for signing */staticchar*get_default_ssh_signing_key(void){
@@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)return"";}+/* Returns a textual but unique representation ot the signing key */+constchar*get_signing_key_id(void)+{+if(!strcmp(use_format->name,"ssh")){+returnget_ssh_key_fingerprint(get_signing_key());+}else{+/* GPG/GPGSM only store a key id on this variable */+returnget_signing_key();+}+}+constchar*get_signing_key(void){if(configured_signing_key)
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf,{conststructref*ref;structstring_list_item*item;-char*signing_key=xstrdup(get_signing_key());+char*signing_key_id=xstrdup(get_signing_key_id());constchar*cp,*np;structstrbufcert=STRBUF_INIT;intupdate_seen=0;strbuf_addstr(&cert,"certificate version 0.1\n");-strbuf_addf(&cert,"pusher %s ",signing_key);+strbuf_addf(&cert,"pusher %s ",signing_key_id);datestamp(&cert);strbuf_addch(&cert,'\n');if(args->url&&*args->url){
@@ -374,7 +374,7 @@ static int generate_push_cert(struct strbuf *req_buf,if(!update_seen)gotofree_return;-if(sign_buffer(&cert,&cert,signing_key))+if(sign_buffer(&cert,&cert,get_signing_key()))die(_("failed to sign the push certificate"));packet_buf_write(req_buf,"push-cert%c%s",0,cap_string);
@@ -386,7 +386,7 @@ static int generate_push_cert(struct strbuf *req_buf,packet_buf_write(req_buf,"push-cert-end\n");free_return:-free(signing_key);+free(signing_key_id);strbuf_release(&cert);returnupdate_seen;}
@@ -137,6 +137,53 @@ test_expect_success GPG 'signed push sends push certificate' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'ssh signed push sends push certificate''+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principalwithnumber1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'inconsistent push options in signed push not allowed''# First, invoke receive-pack with dummy input to obtain its preamble.prepare_dst&&
@@ -276,6 +323,60 @@ test_expect_success GPGSM 'fail without key and heed user.signingkey x509' 'test_cmpexpectdst/push-cert-status'+test_expect_successGPGSSH'fail without key and heed user.signingkey ssh''+test_configgpg.formatssh&&+prepare_dst&&+mkdir-pdst/.git/hooks&&+git-Cdstconfiggpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+git-Cdstconfigreceive.certnonceseedsekrit&&+write_scriptdst/.git/hooks/post-receive<<-\EOF&&+# discard the update list+cat>/dev/null+# record the push certificate+iftest-n"${GIT_PUSH_CERT-}"+then+gitcat-fileblob$GIT_PUSH_CERT>../push-cert+fi&&++cat>../push-cert-status<<E_O_F+SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}+KEY=${GIT_PUSH_CERT_KEY-nokey}+STATUS=${GIT_PUSH_CERT_STATUS-nostatus}+NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}+NONCE=${GIT_PUSH_CERT_NONCE-nononce}+E_O_F++EOF++test_configuser.emailhasnokey@nowhere.com&&+test_configgpg.formatssh&&+test_configuser.signingkey""&&+(+sane_unsetGIT_COMMITTER_EMAIL&&+test_must_failgitpush--signeddstnoopff+noff+)&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+gitpush--signeddstnoopff+noff&&++(+cat<<-\EOF&&+SIGNER=principalwithnumber1+KEY=FINGERPRINT+STATUS=G+NONCE_STATUS=OK+EOF+sed-n-e"s/^nonce /NONCE=/p"-e"/^$/q"dst/push-cert+)|sed-e"s|FINGERPRINT|$FINGERPRINT|">expect&&++noop=$(gitrev-parsenoop)&&+ff=$(gitrev-parseff)&&+noff=$(gitrev-parsenoff)&&+grep"$noop$ff refs/heads/ff"dst/push-cert&&+grep"$noop$noff refs/heads/noff"dst/push-cert&&+test_cmpexpectdst/push-cert-status+'+ test_expect_successGPG'failed atomic push does not execute GPG''prepare_dst&&git-Cdstconfigreceive.certnonceseedsekrit&&
@@ -0,0 +1,161 @@+#!/bin/sh++test_description='signed tag tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed tags ssh''+test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-minitial&&+gittag-s-minitialinitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-msecond&&+gittag-s-msecondsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittag-s-mmergemerge&&++echo4>file&&test_tick&&gitcommit-a-S-m"fourth unsigned"&&+gittag-a-mfourth-unsignedfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittag-s-mfourthfourth-signed&&++echo5>file&&test_tick&&gitcommit-a-m"fifth"&&+gittagfifth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittag-a-msixthsixth-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittag-s-m6thsixth-signedHEAD^&&+gittag-mseventh-sseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth&&+gittag-u"${SIGNING_KEY_UNTRUSTED}"-meightheighth-signed-alt+'++test_expect_successGPGSSH'verify and show ssh signatures''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'detect fudged ssh signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filetagseventh-signed>raw&&+sed-e"/^tag / s/seventh/7th forged/"raw>forged1&&+githash-object-w-ttagforged1>forged1.tag&&+test_must_failgitverify-tag$(catforged1.tag)2>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'verify ssh signatures with --raw''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+fortagininitialsecondmergefourth-signedsixth-signedseventh-signed+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortaginfourth-unsignedfifth-unsignedsixth-unsigned+do+test_must_failgitverify-tag--raw$tag2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)&&+(+fortagineighth-signed-alt+do+gitverify-tag--raw$tag2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$tagOK||exit1+done+)+'++test_expect_successGPGSSH'verify signatures with --raw ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitverify-tag--rawsixth-signed2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echosixth-signedOK+'++test_expect_successGPGSSH'verify multiple tags ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+tags="seventh-signed sixth-signed"&&+foriin$tags+do+gitverify-tag-v--raw$i||return1+done>expect.stdout2>expect.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<expect.stderr.1>expect.stderr&&+gitverify-tag-v--raw$tags>actual.stdout2>actual.stderr.1&&+grep"^${GOOD_SIGNATURE_TRUSTED}"<actual.stderr.1>actual.stderr&&+test_cmpexpect.stdoutactual.stdout&&+test_cmpexpect.stderractual.stderr+'++test_expect_successGPGSSH'verifying tag with --format - ssh''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect<<-\EOF&&+tagname:fourth-signed+EOF+gitverify-tag--format="tagname : %(tag)""fourth-signed">actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'verifying a forged tag with --format should fail silently - ssh''+test_must_failgitverify-tag--format="tagname : %(tag)"$(catforged1.tag)>actual-forged&&+test_must_be_emptyactual-forged+'++test_done
@@ -11,13 +11,13 @@ gpg.program:: gpg.format:: Specifies which key format to use when signing with `--gpg-sign`.- Default is "openpgp" and another possible value is "x509".+ Default is "openpgp". Other possible values are "x509", "ssh". gpg.<format>.program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still be used as a legacy synonym for `gpg.openpgp.program`. The default- value for `gpg.x509.program` is "gpgsm".+ value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen". gpg.minTrustLevel:: Specifies a minimum trust level for signature verification. If
@@ -33,3 +33,38 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate`++gpg.ssh.allowedSignersFile::+ A file containing ssh public keys which you are willing to trust.+ The file consists of one or more lines of principals followed by an ssh+ public key.+ e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...+ See ssh-keygen(1) "ALLOWED SIGNERS" for details.+ The principal is only used to identify the key and is available when+ verifying a signature.+++SSH has no concept of trust levels like gpg does. To be able to differentiate+between valid signatures and trusted signatures the trust level of a signature+verification is set to `fully` when the public key is present in the allowedSignersFile.+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.+Otherwise valid but untrusted signatures will still verify but show no principal+name of the signer.+++This file can be set to a location outside of the repository and every developer+maintains their own trust store. A central repository server could generate this+file automatically from ssh keys with push access to verify the code against.+In a corporate setting this file is probably generated at a global location+from automation that already handles developer ssh keys.+++A repository that only allows signed commits can store the file+in the repository itself using a path relative to the top-level of the working tree.+This way only committers with an already valid key can add or change keys in the keyring.+++Using a SSH CA key with the cert-authority option+(see ssh-keygen(1) "CERTIFICATES") is also valid.++gpg.ssh.revocationFile::+ Either a SSH KRL or a list of revoked public keys (without the principal prefix).+ See ssh-keygen(1) for details.+ If a public key is found in this file then it will always be treated+ as having trust level "never" and signatures will show as invalid.
@@ -36,3 +36,9 @@ user.signingKey:: commit, you can override the default selection with this variable. This option is passed unchanged to gpg's --local-user parameter, so you may specify a key using any method that gpg supports.+ If gpg.format is set to "ssh" this can contain the literal ssh public+ key (e.g.: "ssh-rsa XXXXXX identifier") or a file which contains it and+ corresponds to the private key used for signing. The private key+ needs to be available via ssh-agent. Alternatively it can be set to+ a file containing a private key directly. If not set git will call+ "ssh-add -L" and try to use the first key available.
@@ -0,0 +1,398 @@+#!/bin/sh++test_description='ssh signed commit tests'+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main+exportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME++../test-lib.sh+GNUPGHOME_NOT_USED=$GNUPGHOME+."$TEST_DIRECTORY/lib-gpg.sh"++test_expect_successGPGSSH'create signed commits''+test_oid_cache<<-\EOF&&+headersha1:gpgsig+headersha256:gpgsig-sha256+EOF++test_when_finished"test_unconfig commit.gpgsign"&&+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&++echo1>file&&gitaddfile&&+test_tick&&gitcommit-S-minitial&&+gittaginitial&&+gitbranchside&&++echo2>file&&test_tick&&gitcommit-a-S-msecond&&+gittagsecond&&++gitcheckoutside&&+echo3>elif&&gitaddelif&&+test_tick&&gitcommit-m"third on side"&&++gitcheckoutmain&&+test_tick&&gitmerge-Sside&&+gittagmerge&&++echo4>file&&test_tick&&gitcommit-a-m"fourth unsigned"&&+gittagfourth-unsigned&&++test_tick&&gitcommit--amend-S-m"fourth signed"&&+gittagfourth-signed&&++gitconfigcommit.gpgsigntrue&&+echo5>file&&test_tick&&gitcommit-a-m"fifth signed"&&+gittagfifth-signed&&++gitconfigcommit.gpgsignfalse&&+echo6>file&&test_tick&&gitcommit-a-m"sixth"&&+gittagsixth-unsigned&&++gitconfigcommit.gpgsigntrue&&+echo7>file&&test_tick&&gitcommit-a-m"seventh"--no-gpg-sign&&+gittagseventh-unsigned&&++test_tick&&gitrebase-fHEAD^^&&gittagsixth-signedHEAD^&&+gittagseventh-signed&&++echo8>file&&test_tick&&gitcommit-a-meighth-S"${SIGNING_KEY_UNTRUSTED}"&&+gittageighth-signed-alt&&++# commit.gpgsign is still on but this must not be signed+echo9|gitcommit-treeHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagninth-unsigned$(catoid)&&+# explicit -S of course must sign.+echo10|gitcommit-tree-SHEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtenth-signed$(catoid)&&++# --gpg-sign[=<key-id>] must sign.+echo11|gitcommit-tree--gpg-signHEAD^{tree}>oid&&+test_line_count=1oid&&+gittageleventh-signed$(catoid)&&+echo12|gitcommit-tree--gpg-sign="${SIGNING_KEY_UNTRUSTED}"HEAD^{tree}>oid&&+test_line_count=1oid&&+gittagtwelfth-signed-alt$(catoid)+'++test_expect_successGPGSSH'verify and show signatures''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.mintrustlevelUNDEFINED&&+(+forcommitininitialsecondmergefourth-signed\+fifth-signedsixth-signedseventh-signedtenth-signed\+eleventh-signed+do+gitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsigned\+seventh-unsignedninth-unsigned+do+test_must_failgitverify-commit$commit&&+gitshow--pretty=short--show-signature$commit>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alttwelfth-signed-alt+do+gitshow--pretty=short--show-signature$commit>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'verify-commit exits success on untrusted signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitverify-commiteighth-signed-alt2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+grep"${KEY_NOT_TRUSTED}"actual+'++test_expect_successGPGSSH'verify-commit exits success with matching minTrustLevel''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.minTrustLevelfully&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits success with low minTrustLevel''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configgpg.minTrustLevelmarginal&&+gitverify-commitsixth-signed+'++test_expect_successGPGSSH'verify-commit exits failure with high minTrustLevel''+test_configgpg.minTrustLevelultimate&&+test_must_failgitverify-commiteighth-signed-alt+'++test_expect_successGPGSSH'verify signatures with --raw''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+(+forcommitininitialsecondmergefourth-signedfifth-signedsixth-signedseventh-signed+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitinmerge^2fourth-unsignedsixth-unsignedseventh-unsigned+do+test_must_failgitverify-commit--raw$commit2>actual&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)&&+(+forcommitineighth-signed-alt+do+gitverify-commit--raw$commit2>actual&&+grep"${GOOD_SIGNATURE_UNTRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual&&+echo$commitOK||exit1+done+)+'++test_expect_successGPGSSH'proper header is used for hash algorithm''+gitcat-filecommitfourth-signed>output&&+grep"^$(test_oidheader) -----BEGIN SSH SIGNATURE-----"output+'++test_expect_successGPGSSH'show signed commit with signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitshow-sinitial>commit&&+gitshow-s--show-signatureinitial>show&&+gitverify-commit-vinitial>verify.12>verify.2&&+gitcat-filecommitinitial>cat&&+grep-v-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.commit&&+grep-e"${GOOD_SIGNATURE_TRUSTED}"-e"Warning: "show>show.gpg&&+grep-v"^ "cat|grep-v"^gpgsig.* ">cat.commit&&+test_cmpshow.commitcommit&&+test_cmpshow.gpgverify.2&&+test_cmpcat.commitverify.1+'++test_expect_successGPGSSH'detect fudged signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filecommitseventh-signed>raw&&+sed-e"s/^seventh/7th forged/"raw>forged1&&+githash-object-w-tcommitforged1>forged1.commit&&+test_must_failgitverify-commit$(catforged1.commit)&&+gitshow--pretty=short--show-signature$(catforged1.commit)>actual1&&+grep"${BAD_SIGNATURE}"actual1&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual1&&+!grep"${GOOD_SIGNATURE_UNTRUSTED}"actual1+'++test_expect_successGPGSSH'detect fudged signature with NUL''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcat-filecommitseventh-signed>raw&&+catraw>forged2&&+echoQwik|tr"Q""\000">>forged2&&+githash-object-w-tcommitforged2>forged2.commit&&+test_must_failgitverify-commit$(catforged2.commit)&&+gitshow--pretty=short--show-signature$(catforged2.commit)>actual2&&+grep"${BAD_SIGNATURE}"actual2&&+!grep"${GOOD_SIGNATURE_TRUSTED}"actual2+'++test_expect_successGPGSSH'amending already signed commit''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+gitcheckoutfourth-signed^0&&+gitcommit--amend-S--no-edit&&+gitverify-commitHEAD&&+gitshow-s--show-signatureHEAD>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual&&+!grep"${BAD_SIGNATURE}"actual+'++test_expect_successGPGSSH'show good signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+cat>expect.tmpl<<-\EOF&&+G+FINGERPRINT+principalwithnumber1+FINGERPRINT++EOF+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show bad signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect<<-\EOF&&+B+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catforged1.commit)>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with custom format''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+U+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with undefined trust level''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+undefined+FINGERPRINT++FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"eighth-signed-alt>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_UNTRUSTED}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show untrusted signature with ultimate trust level''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+cat>expect.tmpl<<-\EOF&&+fully+FINGERPRINT+principalwithnumber1+FINGERPRINT++EOF+gitlog-1--format="%GT%n%GK%n%GS%n%GF%n%GP"sixth-signed>actual&&+FINGERPRINT=$(ssh-keygen-lf"${SIGNING_KEY_PRIMARY}"|awk"{print \$2;}")&&+sed"s|FINGERPRINT|$FINGERPRINT|g"expect.tmpl>expect&&+test_cmpexpectactual+'++test_expect_successGPGSSH'show lack of signature with custom format''+cat>expect<<-\EOF&&+N+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"seventh-unsigned>actual&&+test_cmpexpectactual+'++test_expect_successGPGSSH'log.showsignature behaves like --show-signature''+test_configgpg.ssh.allowedSignersFile"${SIGNING_ALLOWED_SIGNERS}"&&+test_configlog.showsignaturetrue&&+gitshowinitial>actual&&+grep"${GOOD_SIGNATURE_TRUSTED}"actual+'++test_expect_successGPGSSH'check config gpg.format values''+test_configgpg.formatssh&&+test_configuser.signingkey"${SIGNING_KEY_PRIMARY}"&&+test_configgpg.formatssh&&+gitcommit-S--amend-m"success"&&+test_configgpg.formatOpEnPgP&&+test_must_failgitcommit-S--amend-m"fail"+'++test_expect_failureGPGSSH'detect fudged commit with double signature (TODO)''+sed-e"/gpgsig/,/END PGP/d"forged1>double-base&&+sed-n-e"/gpgsig/,/END PGP/p"forged1|\+sed-e"s/^$(test_oidheader)//;s/^ //"|gpg--dearmor>double-sig1.sig&&+gpg-odouble-sig2.sig-u29472784--detach-signdouble-base&&+catdouble-sig1.sigdouble-sig2.sig|gpg--enarmor>double-combined.asc&&+sed-e"s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oidheader) /;2,\$s/^/ /"\+double-combined.asc>double-gpgsig&&+sed-e"/committer/r double-gpgsig"double-base>double-commit&&+githash-object-w-tcommitdouble-commit>double-commit.commit&&+test_must_failgitverify-commit$(catdouble-commit.commit)&&+gitshow--pretty=short--show-signature$(catdouble-commit.commit)>double-actual&&+grep"BAD signature from"double-actual&&+grep"Good signature from"double-actual+'++test_expect_failureGPGSSH'show double signature with custom format (TODO)''+cat>expect<<-\EOF&&+E+++++EOF+gitlog-1--format="%G?%n%GK%n%GS%n%GF%n%GP"$(catdouble-commit.commit)>actual&&+test_cmpexpectactual+'+++test_expect_failureGPGSSH'verify-commit verifies multiply signed commits (TODO)''+gitinitmultiply-signed&&+cdmultiply-signed&&+test_commitfirst&&+echo1>second&&+gitaddsecond&&+tree=$(gitwrite-tree)&&+parent=$(gitrev-parseHEAD^{commit})&&+gitcommit--gpg-sign-msecond&&+gitcat-filecommitHEAD&&+# Avoid trailing whitespace.+sed-e"s/^Q//"-e"s/^Z/ /">commit<<-EOF&&+Qtree$tree+Qparent$parent+QauthorAUThor<author@example.com>1112912653-0700+QcommitterCOMitter<committer@example.com>1112912653-0700+Qgpgsig-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC+QAKCTqBh+VabJceXcGIZuF0Ry+udbBQ==+Q=tQ0N+Q-----ENDPGPSIGNATURE-----+Qgpgsig-sha256-----BEGINPGPSIGNATURE-----+QZ+QiHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy+QQGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO+QAJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==+Q=pIwP+Q-----ENDPGPSIGNATURE-----+Q+Qsecond+EOF+head=$(githash-object-tcommit-wcommit)&&+gitreset--hard$head&&+gitverify-commit$head2>actual&&+grep"Good signature from"actual&&+!grep"BAD signature from"actual+'++test_done
From: Jonathan Tan <hidden> Date: 2021-07-28 22:32:14
I think this patch set is beyond the "is this a good idea in general"
phase (in particular, I think that being able to sign Git commits by
using SSH infrastructure is very useful), so I'll proceed to critiquing
the commits in more detail.
Firstly, in commit messages, the left side of the colon is usually the
name of the subsystem - in this case, "gpg-interface".
To be able to implement new signing formats this commit:
- makes the sigc structure more generic by renaming "gpg_output" to
"output"
- introduces function pointers in the gpg_format structure to call
format specific signing and verification functions
- moves format detection from verify_signed_buffer into the check_signature
api function and calls the format specific verify
- renames and wraps sign_buffer to handle format specific signing logic
as well
I think that this commit should be further split up - in particular, it
is hard for reviewers to verify that there is no difference in
functionality before and after this commit. I already spotted one
difference - perhaps there are more. For me, splitting the above 4
points into 4 commits would be an acceptable split.
I think that verify_signed_buffer and sign_buffer should replace
verify_args and sigs, not be alongside them. In particular, I see from
later patches that a new entry will be introduced for SSH, and the
corresponding new "verify" function does not use verify_args or sigs.
From: Jonathan Tan <hidden> Date: 2021-07-28 22:45:30
Keep the commit titles to 50 characters or fewer. E.g.:
gpg-interface: teach "ssh" gpg.format
implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function
Capitalization and punctuation.
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.
gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.
I notice that end-user documentation (e.g. about gpg.ssh.program) is in
its own patch, but could that be added as functionality is being
implemented? That makes it easier for reviewers to understand what's
being implemented in each patch.
quoted hunk
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig return use_format->sign_buffer(buffer, signature, signing_key); }+/*+ * Strip CR from the line endings, in case we are on Windows.+ * NEEDSWORK: make it trim only CRs before LFs and rename+ */+static void remove_cr_after(struct strbuf *buffer, size_t offset)+{+ size_t i, j;++ for (i = j = offset; i < buffer->len; i++) {+ if (buffer->buf[i] != '\r') {+ if (i != j)+ buffer->buf[j] = buffer->buf[i];+ j++;+ }+ }+ strbuf_setlen(buffer, j);+}
In the future, I would prefer refactoring like this to be in its own
patch. For the moment, this should probably be called "remove_cr" (no
"after" as CRs are removed wherever they are in the string).
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+ const char *signing_key)
+{
+ struct child_process signer = CHILD_PROCESS_INIT;
+ int ret = -1;
+ size_t bottom, keylen;
+ struct strbuf signer_stderr = STRBUF_INIT;
+ struct tempfile *key_file = NULL, *buffer_file = NULL;
+ char *ssh_signing_key_file = NULL;
+ struct strbuf ssh_signature_filename = STRBUF_INIT;
+
+ if (!signing_key || signing_key[0] == '\0')
+ return error(
+ _("user.signingkey needs to be set for ssh signing"));
+
+ if (starts_with(signing_key, "ssh-")) {
+ /* A literal ssh key */
+ key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
+ if (!key_file)
+ return error_errno(
+ _("could not create temporary file"));
+ keylen = strlen(signing_key);
+ if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
+ close_tempfile_gently(key_file) < 0) {
+ error_errno(_("failed writing ssh signing key to '%s'"),
+ key_file->filename.buf);
+ goto out;
+ }
+ ssh_signing_key_file = key_file->filename.buf;
+ } else {
+ /* We assume a file */
+ ssh_signing_key_file = expand_user_path(signing_key, 1);
+ }
A config that has 2 modes of operation is quite error-prone, I think.
For example, a user could put a path starting with "ssh-" (admittedly
unlikely since it would usually be an absolute path, but not
impossible). And also from an implementation point of view, here the
"ssh-" is case-sensitive, but in a future patch, there is a "ssh-" that
is case-insensitive.
Can this just always take a path?
+ if (ret) {
+ if (strstr(signer_stderr.buf, "usage:"))
+ error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
+
+ error("%s", signer_stderr.buf);
+ goto out;
+ }
Checking for "usage:" seems fragile - a binary running in a different
locale might emit a different string, and legitimate output may somehow
contain the string "usage:". Is there a different way to detect a
version mismatch?
From: Jonathan Tan <hidden> Date: 2021-07-28 22:48:36
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
[snip]
+/* Returns the first public key from an ssh-agent to use for signing */
+static char *get_default_ssh_signing_key(void)
+{
+ struct child_process ssh_add = CHILD_PROCESS_INIT;
+ int ret = -1;
+ struct strbuf key_stdout = STRBUF_INIT;
+ struct strbuf **keys;
+
+ strvec_pushl(&ssh_add.args, "ssh-add", "-L", NULL);
+ ret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);
+ if (!ret) {
+ keys = strbuf_split_max(&key_stdout, '\n', 2);
+ if (keys[0])
+ return strbuf_detach(keys[0], NULL);
+ }
+
+ strbuf_release(&key_stdout);
+ return "";
+}
Could the commit message have a better explanation of why we need this?
(Also, I would think that the command being run needs to be configurable
instead of being just the first "ssh-add" in $PATH, and the parsing of
the output should be more rigorous. But this is moot if we don't need
this feature in the first place.)
From: Jonathan Tan <hidden> Date: 2021-07-28 23:04:56
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).
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?
On 29/07/21 02.36, Fabian Stelzer via GitGitGadget wrote:
openssh 8.7 will add valid-after, valid-before options to the allowed keys
keyring. This allows us to pass the commit timestamp to the verification
call and make key rollover possible and still be able to verify older
commits. Set valid-after=NOW when adding your key to the keyring and set
valid-before to make it fail if used after a certain date. Software like
gitolite/github or corporate automation can do this automatically when ssh
push keys are addded / removed I will add this feature in a follow up patch
afterwards.
I read above as "set valid-before=<some date> and valid-after=<now> to
limit key validity for several days from now". Is it right?
--
An old man doll... just what I always wanted! - Clara
I think this patch set is beyond the "is this a good idea in general"
phase (in particular, I think that being able to sign Git commits by
using SSH infrastructure is very useful), so I'll proceed to critiquing
the commits in more detail.
Thanks for your help.
Firstly, in commit messages, the left side of the colon is usually the
name of the subsystem - in this case, "gpg-interface".
The docs call this "name of the component you're working on". Since this
code does not actually change any gpg functionality (at least it should
not) i think gpg-interface in the commits might be a bit misleading.
quoted
To be able to implement new signing formats this commit:
- makes the sigc structure more generic by renaming "gpg_output" to
"output"
- introduces function pointers in the gpg_format structure to call
format specific signing and verification functions
- moves format detection from verify_signed_buffer into the check_signature
api function and calls the format specific verify
- renames and wraps sign_buffer to handle format specific signing logic
as well
I think that this commit should be further split up - in particular, it
is hard for reviewers to verify that there is no difference in
functionality before and after this commit. I already spotted one
difference - perhaps there are more. For me, splitting the above 4
points into 4 commits would be an acceptable split.
The rename can of course be easily separated. The others would probably
require some code in between commits that's not present in the final
patch result to make the individual commits compile / work. Otherwise
those would only add unused code with the last commit then actually
using everything. I don't think that would make things easier to verify,
would it?
I think that verify_signed_buffer and sign_buffer should replace
verify_args and sigs, not be alongside them. In particular, I see from
later patches that a new entry will be introduced for SSH, and the
corresponding new "verify" function does not use verify_args or sigs.
I kept the verify_args since i would either have to duplicate the
verify_gpg_signed_buffer for gpg & gpgsm or have an if within deciding
what format to use.
Also this is something that we might want to make a configuration option
in the future and pass to ssh-keygen as well (there are a couple of -O
options for it users might want)
sigs is still needed for the parse_signed_buffer api function.
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
[snip]
Could the commit message have a better explanation of why we need this?
(Also, I would think that the command being run needs to be configurable
instead of being just the first "ssh-add" in $PATH, and the parsing of
the output should be more rigorous. But this is moot if we don't need
this feature in the first place.)
How about:
If user.signingkey ist not set and a ssh signature is requested we call
ssh-add -L und use the first key we get. This enables us to activate
commit signing globally for all users on a shared server when ssh-agent
forwarding is already in use without the need to touch an individual
users gitconfig.
Maybe a general gpg.ssh.signingKeyDefaultCommand that we call and use
the first returned line as key would be useful and achieve the same goal
without having this default for everyone.
On the other hand i like having less configuration / good defaults for
individual users. But I'm coming from a corporate environment, not an
open source project.
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.
quoted
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.
quoted
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.
quoted
+ 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
quoted
+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 ");
Keep the commit titles to 50 characters or fewer. E.g.:
gpg-interface: teach "ssh" gpg.format
i will go over my commits and shorten them although I find your example
very unclear. or did you mean: teach "ssh" to gpg.format ?
quoted
implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function
Capitalization and punctuation.
fixed
quoted
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.
gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.
I notice that end-user documentation (e.g. about gpg.ssh.program) is in
its own patch, but could that be added as functionality is being
implemented? That makes it easier for reviewers to understand what's
being implemented in each patch.
I can move the user.signingkey & gpg.format part into the signing
implementation commit and the rest into the verification. I don't see
much benefit in splitting it up further. I don't want to split up parts
of the same documentation block into separate commits.
A config that has 2 modes of operation is quite error-prone, I think.
For example, a user could put a path starting with "ssh-" (admittedly
unlikely since it would usually be an absolute path, but not
impossible). And also from an implementation point of view, here the
"ssh-" is case-sensitive, but in a future patch, there is a "ssh-" that
is case-insensitive.
Can this just always take a path?
I found the ability to specify the key literally useful since i don't
need an extra file for my public key. In my case all keys come from an
ssh-agent anyway but I'd like to be able to select which one to use for
signing. But i'm not hard pressed on this feature. If consenus is this
complicates things then i can remove it.
quoted
+ if (ret) {
+ if (strstr(signer_stderr.buf, "usage:"))
+ error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
+
+ error("%s", signer_stderr.buf);
+ goto out;
+ }
Checking for "usage:" seems fragile - a binary running in a different
locale might emit a different string, and legitimate output may somehow
contain the string "usage:". Is there a different way to detect a
version mismatch?
I agree. Unfortunately i did not find any better way. But i think the
risk of doing something wrong here is quite low. We only check for
"usage:" in case ssh-keygen fails. And all we do if we find it is give
the user an extra hint on what the problem probably is.
In any case we print the full stderr output as well.
On 29/07/21 02.36, Fabian Stelzer via GitGitGadget wrote:
quoted
openssh 8.7 will add valid-after, valid-before options to the allowed
keys
keyring. This allows us to pass the commit timestamp to the verification
call and make key rollover possible and still be able to verify older
commits. Set valid-after=NOW when adding your key to the keyring and set
valid-before to make it fail if used after a certain date. Software like
gitolite/github or corporate automation can do this automatically when
ssh
push keys are addded / removed I will add this feature in a follow up
patch
afterwards.
I read above as "set valid-before=<some date> and valid-after=<now> to
limit key validity for several days from now". Is it right?
no. "NOW" is not meant literally but in the sense to add the current
date when adding the key. I'll edit the description. But this feature in
general will follow in a separate patchset with proper documentation anyway.
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".
Thanks for this series, it sounds like a great idea. I have a few
comments, inline below.
On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:
[snip]
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+ const char *signing_key)
+{
+ struct child_process signer = CHILD_PROCESS_INIT;
+ int ret = -1;
+ size_t bottom, keylen;
+ struct strbuf signer_stderr = STRBUF_INIT;
+ struct tempfile *key_file = NULL, *buffer_file = NULL;
+ char *ssh_signing_key_file = NULL;
+ struct strbuf ssh_signature_filename = STRBUF_INIT;
+
+ if (!signing_key || signing_key[0] == '\0')
+ return error(
+ _("user.signingkey needs to be set for ssh signing"));
+
+ if (starts_with(signing_key, "ssh-")) {
+ /* A literal ssh key */
+ key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
+ if (!key_file)
+ return error_errno(
+ _("could not create temporary file"));
+ keylen = strlen(signing_key);
+ if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
+ close_tempfile_gently(key_file) < 0) {
+ error_errno(_("failed writing ssh signing key to '%s'"),
+ key_file->filename.buf);
+ goto out;
+ }
+ ssh_signing_key_file = key_file->filename.buf;
You probably want to call strbuf_detach() here, because...
+ } else {
+ /* We assume a file */
+ ssh_signing_key_file = expand_user_path(signing_key, 1);
+ }
... you need to free the memory returned by expand_user_path(). If you
detach the strbuf above, you can unconditionally
free(ssh_signing_key_file) at the end of this function.
+
+ buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
+ if (!buffer_file) {
+ error_errno(_("could not create temporary file"));
+ goto out;
+ }
+
+ if (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||
+ close_tempfile_gently(buffer_file) < 0) {
+ error_errno(_("failed writing ssh signing key buffer to '%s'"),
+ buffer_file->filename.buf);
+ goto out;
+ }
+
+ strvec_pushl(&signer.args, use_format->program,
+ "-Y", "sign",
+ "-n", "git",
+ "-f", ssh_signing_key_file,
+ buffer_file->filename.buf,
+ NULL);
+
+ sigchain_push(SIGPIPE, SIG_IGN);
+ ret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);
+ sigchain_pop(SIGPIPE);
+
+ if (ret) {
+ if (strstr(signer_stderr.buf, "usage:"))
+ error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
I share Jonathan Tan's concern about checking for "usage:" in the stderr
output here. I think in patch 6 the tests rely on a specific return code
to check that "-Y sign" is working as expected; can that be used here
instead?
+
+ error("%s", signer_stderr.buf);
+ goto out;
+ }
+
+ bottom = signature->len;
+
+ strbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);
+ strbuf_addstr(&ssh_signature_filename, ".sig");
+ if (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {
+ error_errno(
+ _("failed reading ssh signing data buffer from '%s'"),
+ ssh_signature_filename.buf);
+ }
+ unlink_or_warn(ssh_signature_filename.buf);
+
+ /* Strip CR from the line endings, in case we are on Windows. */
+ remove_cr_after(signature, bottom);
+
+out:
+ if (key_file)
+ delete_tempfile(&key_file);
+ if (buffer_file)
+ delete_tempfile(&buffer_file);
+ strbuf_release(&signer_stderr);
+ strbuf_release(&ssh_signature_filename);
+ return ret;
+}
--
gitgitgadget
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
[snip]
Could the commit message have a better explanation of why we need this?
(Also, I would think that the command being run needs to be configurable
instead of being just the first "ssh-add" in $PATH, and the parsing of
the output should be more rigorous. But this is moot if we don't need
this feature in the first place.)
How about:
If user.signingkey ist not set and a ssh signature is requested we call
ssh-add -L und use the first key we get. This enables us to activate commit
signing globally for all users on a shared server when ssh-agent forwarding
is already in use without the need to touch an individual users gitconfig.
Maybe a general gpg.ssh.signingKeyDefaultCommand that we call and use the
first returned line as key would be useful and achieve the same goal without
having this default for everyone.
On the other hand i like having less configuration / good defaults for
individual users. But I'm coming from a corporate environment, not an open
source project.
Doesn't this run the risk of using the wrong key (and potentially
exposing someone's identity)? On my work machine, my corporate SSH key
is not actually the first key in my SSH agent.
Rather than making this behavior the default, could it instead be
enabled only if the signing key is set to "use-ssh-agent" or something
similar?
@@ -87,6 +87,35 @@ test_lazy_prereq RFC1991 'echo|gpg--homedir"${GNUPGHOME}"-b--rfc1991>/dev/null'+test_lazy_prereqGPGSSH'+ssh_version=$(ssh-keygen-Yfind-principals-n"git"2>&1)+test$?!=127||exit1+echo$ssh_version|grep-q"find-principals:missing signature file"+test$?=0||exit1;+mkdir-p"${GNUPGHOME}"&&+chmod0700"${GNUPGHOME}"&&+ssh-keygen-ted25519-N""-C"git ed25519 key"-f"${GNUPGHOME}/ed25519_ssh_signing_key">/dev/null&&+echo"\"principal with number 1\" $(cat"${GNUPGHOME}/ed25519_ssh_signing_key.pub")">>"${GNUPGHOME}/ssh.all_valid.allowedSignersFile"&&+ssh-keygen-trsa-b2048-N""-C"git rsa2048 key"-f"${GNUPGHOME}/rsa_2048_ssh_signing_key">/dev/null&&+echo"\"principal with number 2\" $(cat"${GNUPGHOME}/rsa_2048_ssh_signing_key.pub")">>"${GNUPGHOME}/ssh.all_valid.allowedSignersFile"&&+ssh-keygen-ted25519-N"super_secret"-C"git ed25519 encrypted key"-f"${GNUPGHOME}/protected_ssh_signing_key">/dev/null&&+echo"\"principal with number 3\" $(cat"${GNUPGHOME}/protected_ssh_signing_key.pub")">>"${GNUPGHOME}/ssh.all_valid.allowedSignersFile"&&+cat"${GNUPGHOME}/ssh.all_valid.allowedSignersFile"&&+ssh-keygen-ted25519-N""-f"${GNUPGHOME}/untrusted_ssh_signing_key">/dev/null+'++SIGNING_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key"+SIGNING_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key"+SIGNING_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key"+SIGNING_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key"+SIGNING_KEY_PASSPHRASE="super_secret"+SIGNING_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile"++GOOD_SIGNATURE_TRUSTED='Good "git" signature for'+GOOD_SIGNATURE_UNTRUSTED='Good "git" signature with'+KEY_NOT_TRUSTED="No principal matched"+BAD_SIGNATURE="Signature verification failed"+
Is there a reason why we don't use these variables in the script above?
Also, in general I feel that it's better to add tests in the same commit
where new features are added, rather than having standalone test
commits.
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
[snip]
Could the commit message have a better explanation of why we need this?
(Also, I would think that the command being run needs to be configurable
instead of being just the first "ssh-add" in $PATH, and the parsing of
the output should be more rigorous. But this is moot if we don't need
this feature in the first place.)
How about:
If user.signingkey ist not set and a ssh signature is requested we call
ssh-add -L und use the first key we get. This enables us to activate commit
signing globally for all users on a shared server when ssh-agent forwarding
is already in use without the need to touch an individual users gitconfig.
Maybe a general gpg.ssh.signingKeyDefaultCommand that we call and use the
first returned line as key would be useful and achieve the same goal without
having this default for everyone.
On the other hand i like having less configuration / good defaults for
individual users. But I'm coming from a corporate environment, not an open
source project.
Doesn't this run the risk of using the wrong key (and potentially
exposing someone's identity)? On my work machine, my corporate SSH key
is not actually the first key in my SSH agent.
Rather than making this behavior the default, could it instead be
enabled only if the signing key is set to "use-ssh-agent" or something
similar?
If we introduce a signingKeyDefaultComand we don't need the
"use-ssh-agent" flag.
If user.signingkey is set it is used no matter what. A private key needs
to be available either in the specified file or via ssh agent.
If it is not set then an automatic way to get a default key would be great.
So if we set signingKeyDefaultCommand to "ssh-add" (or a script
returning a key) then the first available key could be used.
If this variable is unset and no user.signingkey is specified we fail
and tell the user to set a signingkey.
If this variable is set to "ssh-add" by default or unset and needs to be
set explicitly set to have an automatic default key can be decided.
Thanks for this series, it sounds like a great idea. I have a few
comments, inline below.
Thanks for your review and help with this patch.
On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:
[snip]
quoted
+ ssh_signing_key_file = key_file->filename.buf;
You probably want to call strbuf_detach() here, because...
quoted
+ } else {
+ /* We assume a file */
+ ssh_signing_key_file = expand_user_path(signing_key, 1);
+ }
... you need to free the memory returned by expand_user_path(). If you
detach the strbuf above, you can unconditionally
free(ssh_signing_key_file) at the end of this function.
fixed. thanks
quoted
+
+ buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
+ if (!buffer_file) {
+ error_errno(_("could not create temporary file"));
+ goto out;
+ }
+
+ if (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||
+ close_tempfile_gently(buffer_file) < 0) {
+ error_errno(_("failed writing ssh signing key buffer to '%s'"),
+ buffer_file->filename.buf);
+ goto out;
+ }
+
+ strvec_pushl(&signer.args, use_format->program,
+ "-Y", "sign",
+ "-n", "git",
+ "-f", ssh_signing_key_file,
+ buffer_file->filename.buf,
+ NULL);
+
+ sigchain_push(SIGPIPE, SIG_IGN);
+ ret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);
+ sigchain_pop(SIGPIPE);
+
+ if (ret) {
+ if (strstr(signer_stderr.buf, "usage:"))
+ error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
I share Jonathan Tan's concern about checking for "usage:" in the stderr
output here. I think in patch 6 the tests rely on a specific return code
to check that "-Y sign" is working as expected; can that be used here
instead?
In the test setup i first check if ssh-keygen at all is present (exit
code 127 means command not found). Afterwards i check for a specific
error message from the command if it is present. Not sure how portable
this is, but i can do that because i give known invalid parameters to
it. I can't do this here without doing an additional call to ssh-keygen
just to check this.
quoted
+
+ error("%s", signer_stderr.buf);
+ goto out;
+ }
+
+ bottom = signature->len;
+
+ strbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);
+ strbuf_addstr(&ssh_signature_filename, ".sig");
+ if (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {
+ error_errno(
+ _("failed reading ssh signing data buffer from '%s'"),
+ ssh_signature_filename.buf);
+ }
+ unlink_or_warn(ssh_signature_filename.buf);
+
+ /* Strip CR from the line endings, in case we are on Windows. */
+ remove_cr_after(signature, bottom);
+
+out:
+ if (key_file)
+ delete_tempfile(&key_file);
+ if (buffer_file)
+ delete_tempfile(&buffer_file);
+ strbuf_release(&signer_stderr);
+ strbuf_release(&ssh_signature_filename);
+ return ret;
+}
--
gitgitgadget
Is there a reason why we don't use these variables in the script above?
Also, in general I feel that it's better to add tests in the same commit
where new features are added, rather than having standalone test
commits.
Intially i wanted to fill them in the prereq but couldn't acces them in
the tests then.
Thanks, i have moved the variables above the prereq and used them there
as well. makes sense.
Also i have prefixed them now with GPGSSH so we don't collide with any
other tests accidentally.
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.
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.
From: Fabian Stelzer via GitGitGadget <hidden> Date: 2021-08-03 13:46:06
From: Fabian Stelzer <redacted>
Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.
In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.
To be able to implement new signing formats this commit:
- makes the sigc structure more generic by renaming "gpg_output" to
"output"
- introduces function pointers in the gpg_format structure to call
format specific signing and verification functions
- moves format detection from verify_signed_buffer into the check_signature
api function and calls the format specific verify
- renames and wraps sign_buffer to handle format specific signing logic
as well
Signed-off-by: Fabian Stelzer <redacted>
---
fmt-merge-msg.c | 6 +--
gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
gpg-interface.h | 2 +-
log-tree.c | 8 ++--
pretty.c | 4 +-
5 files changed, 74 insertions(+), 50 deletions(-)
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,"--verify",temp->filename.buf,"-",NULL);-if(!gpg_status)-gpg_status=&buf;-sigchain_push(SIGPIPE,SIG_IGN);-ret=pipe_command(&gpg,payload,payload_size,-gpg_status,0,gpg_output,0);+ret=pipe_command(&gpg,payload,payload_size,&gpg_stdout,0,+&gpg_stderr,0);sigchain_pop(SIGPIPE);delete_tempfile(&temp);-ret|=!strstr(gpg_status->buf,"\n[GNUPG:] GOODSIG ");-strbuf_release(&buf);/* no matter it was used or not */+ret|=!strstr(gpg_stdout.buf,"\n[GNUPG:] GOODSIG ");+sigc->payload=xmemdupz(payload,payload_size);+sigc->output=strbuf_detach(&gpg_stderr,NULL);+sigc->gpg_status=strbuf_detach(&gpg_stdout,NULL);++parse_gpg_output(sigc);++strbuf_release(&gpg_stdout);+strbuf_release(&gpg_stderr);returnret;}
@@ -585,8 +585,8 @@ static int show_one_mergetag(struct commit *commit,/* could have a good signature */status=check_signature(payload.buf,payload.len,signature.buf,signature.len,&sigc);-if(sigc.gpg_output)-strbuf_addstr(&verify_message,sigc.gpg_output);+if(sigc.output)+strbuf_addstr(&verify_message,sigc.output);elsestrbuf_addstr(&verify_message,"No signature\n");signature_check_clear(&sigc);