Add commit & tag signing/verification via SSH keys using ssh-keygen
PATCH 1/1 Add commit & tag signing/verification via SSH keys using ssh-keygen
From: Fabian Stelzer <[email protected]>
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
---
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(-)
Documentation/config/gpg.txt +11 -2
@@ -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`
Documentation/config/user.txt +4 -0
@@ -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.
gpg-interface.c +187 -25
291 lines not on this page · raw message
gpg-interface.h +3 -0
@@ -7,6 +7,8 @@ struct strbuf;#define GPG_VERIFY_RAW 2#define GPG_VERIFY_OMIT_STATUS 4 +#define GPG_SSH_ALLOWED_SIGNERS ".gitsigners" +enum signature_trust_level {TRUST_UNDEFINED ,TRUST_NEVER ,
@@ -64,6 +66,7 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); +const char *get_ssh_allowed_signers (void );int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc);
base-commit: 670b81a890388c60b7032a4f5b879f2ece8c4558
Signature · 1 line
gitgitgadget
Quoted 12 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) 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 8 lines
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?
Signature · 11 lines
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
Quoted 24 lines
Quoted 11 lines
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) 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 8 lines
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.
Quoted 7 lines
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.
Quoted 6 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) and commits/tags can be signed using the private key from your ssh-agent.
Quoted 6 lines
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.
Quoted 4 lines
Signed-off-by Fabian Stelzer <[email protected]>---
RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
Quoted 10 lines
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?
Quoted 9 lines
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_signersfile 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.
Quoted 8 lines
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.
Quoted 21 lines
Documentation/config/gpg.txt +2
-2@@ -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
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 34
Quoted 8 lines
Documentation/config/gpg.txt +5
-0@@ -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.
Signature · 2 lines
brian m. carlson (he/him or they/them) Toronto, Ontario, CA
Quoted 10 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) 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.
There are probably style and coding-guideline nit people will pick in the patch, but first of all I have to say that I am uncomfortably excited to see this addition.
One thing that is unclear is how the 'allowed-signers' is expected to be maintained in the larger picture. Who decides which keys (belong to whom) are trustworthy? Does a contributor has to agree with the decision that certain keys are trustworthy made by somebody else in the project and use the same 'allowed-signers' collection of keys to effectively participate in the project? How do revoking and rotating keys work?
It was a deliberate design decision to let PGP infrastructure that is used to sign and verify signatures when we use PGP for signing without tying any of these decisions to the tracked contents, as that would reduce the attack surface for a malicious tree contents to affect the signing and verification (in other words, "we punted" ;-). Even though I am not sure exactly what you meant by "defaults to .gitsigners", I am assuming that you meant a file with the name at the top-level of the working tree, which makes me worried, as it opens us to the risk of reading from and blindly trusting whatever somebody else placed in the tree contents immediately after we "git pull" (or "git clone").
Thanks for working on it.
From: Fabian Stelzer <[email protected]>
Quoted 4 lines
Quoted 4 lines
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) and commits/tags can be signed using the private key from your ssh-agent.
Quoted 8 lines
Quoted 6 lines
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 15 lines
Quoted 5 lines
Signed-off-by Fabian Stelzer <[email protected]>---
RFC: Add commit & tag signing/verification via SSH keys using ssh-keygenQuoted 10 lines
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.
Quoted 7 lines
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 24 lines
Quoted 10 lines
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_signersfile 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.Quoted 8 lines
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.
Quoted 5 lines
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.
Quoted 19 lines
Quoted 19 lines
Documentation/config/gpg.txt +2
-2@@ -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".
Quoted 14 lines
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 34
Quoted 11 lines
Documentation/config/gpg.txt +5
-0gpg.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 .gitsignersWe 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.
Quoted 37 lines
Quoted 10 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) 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.There are probably style and coding-guideline nit people will pick in the patch, but first of all I have to say that I am uncomfortably excited to see this addition.
One thing that is unclear is how the 'allowed-signers' is expected to be maintained in the larger picture. Who decides which keys (belong to whom) are trustworthy? Does a contributor has to agree with the decision that certain keys are trustworthy made by somebody else in the project and use the same 'allowed-signers' collection of keys to effectively participate in the project? How do revoking and rotating keys work?
It was a deliberate design decision to let PGP infrastructure that is used to sign and verify signatures when we use PGP for signing without tying any of these decisions to the tracked contents, as that would reduce the attack surface for a malicious tree contents to affect the signing and verification (in other words, "we punted" ;-). Even though I am not sure exactly what you meant by "defaults to .gitsigners", I am assuming that you meant a file with the name at the top-level of the working tree, which makes me worried, as it opens us to the risk of reading from and blindly trusting whatever somebody else placed in the tree contents immediately after we "git pull" (or "git clone").
Quoted 1 line
Thanks for working on it.
Glad to hear that :)
I tried to keep the style with the existing code but the IDE sometimes has its own idea.
I think there are two basic options for maintaining the allowed signers file:
1. Every developer has their own stored outside of the repo and adds/revokes trust manually like with gpg.
A central repo would probably verify against a list managed by the tool (e.g. gitolite)
2. Store it in a .gitsigners file in the repo. This would only work if you only allow signed commits/pushes from this point onwards. But this way a shared understading of trusted users can be maintained easily. Only already trusted committers can add new users or change their own keys. The signers file is basically a ssh_authorized_keys file with an additional principal identifier added at the front like:
[email protected] ssh-rsa XXXKEYXXX Comment
[email protected] ssh-ed25519 XXXKEYXXX Comment
Where are commits usually verified at the moment? On every devs checkout or only centrally on pushes?
The signers file also supports SSH CA keys and wildcard identifiers. At the moment i look up the principal dynamically via the public key so it's just a text info of who's key is it at the moment.
The SSH CA Stuff is probably a niche use case but could be cool in a corporate setting. Thats also what the revocation file is used for. The SSH CA can generate a KRL (like crl) which you put into it or you can specify explicit public keys in it to deny them.
Quoted 5 lines · Fabian Stelzer
Quoted 4 lines
One thing that is unclear is how the 'allowed-signers' is expected to be maintained in the larger picture. Who decides which keys
...
Thanks for working on it.Glad to hear that :)
Thanks for explaining your thoughts (omitted). When I say "this is unclear" in my response to a patch, I expect that unclear-ness will be shared by other readers of the code, the doc and/or the log message, so please make sure an updated patch will reduce the need to ask the same question by future readers.
Quoted 2 lines
I tried to keep the style with the existing code but the IDE sometimes has its own idea.
Documentation/CodingGuidelines and Documentation/SubmittingPatches would hopefully help (if not, please ask and/or suggest clarification on these documents).
Thanks.
Quoted 51 lines · Fabian Stelzer
Quoted 36 lines
Quoted 10 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) 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.There are probably style and coding-guideline nit people will pick in the patch, but first of all I have to say that I am uncomfortably excited to see this addition.
One thing that is unclear is how the 'allowed-signers' is expected to be maintained in the larger picture. Who decides which keys (belong to whom) are trustworthy? Does a contributor has to agree with the decision that certain keys are trustworthy made by somebody else in the project and use the same 'allowed-signers' collection of keys to effectively participate in the project? How do revoking and rotating keys work?
It was a deliberate design decision to let PGP infrastructure that is used to sign and verify signatures when we use PGP for signing without tying any of these decisions to the tracked contents, as that would reduce the attack surface for a malicious tree contents to affect the signing and verification (in other words, "we punted"
;-). Even though I am not sure exactly what you meant by "defaults to .gitsigners", I am assuming that you meant a file with the name at the top-level of the working tree, which makes me worried, as it opens us to the risk of reading from and blindly trusting whatever somebody else placed in the tree contents immediately after we "git pull" (or "git clone").Quoted 1 line
Thanks for working on it.
Glad to hear that :)
I tried to keep the style with the existing code but the IDE sometimes has its own idea.I think there are two basic options for maintaining the allowed signers
file:
1. Every developer has their own stored outside of the repo and adds/revokes trust manually like with gpg.
A central repo would probably verify against a list managed by the tool (e.g. gitolite) 2. Store it in a .gitsigners file in the repo. This
would only work if you only allow signed commits/pushes from this point onwards. But this way a shared understading of trusted users can be maintained easily.
Only already trusted committers can add new users or change their own keys. The signers file is basically assh_authorized_keysfile
with an additional principal identifier added at the front like:
[email protected] ssh-rsa XXXKEYXXX Comment
[email protected] ssh-ed25519 XXXKEYXXX Comment
Quoted 6 lines
Where are commits usually verified at the moment? On every devs checkout or only centrally on pushes?
The signers file also supports SSH CA keys and wildcard identifiers. At the moment i look up the principal dynamically via the public key so it's just a text info of who's key is it at the moment.
The SSH CA Stuff is probably a niche use case but could be cool in a corporate setting. Thats also what the revocation file is used for. The SSH CA can generate a KRL (like crl) which you put into it or you can specify explicit public keys in it to deny them.
Just musing here... If adding SSH CA, would not adding support for a self-signed SSL CA make sense? In such a situation, a self-signed certificate can be created at an organizational level, or even from an official root CA. Per-user self-signed certificates, or organizationally defined CAs and certificates, could be created that are more stable than SSH CAs. Then something like OpenSSL (via libcurl) could handle the signature and validation management. Signed content could propagate to Cloud-based git servers and retain their ability to be property verified. Although I can see a drawback here, which relates to expiring certificates - although the concept of an expired signed content is somewhat compelling. Imaging the use case where a company has an employee who signs a tag/commit. The employee departs/retires/terminated/etc., and with it the published certificate is also revoked - an extreme case perhaps, but if the code can no longer be trusted by virtue of the termination, maybe t
his is semantically interesting. This could be a core git function with no additional dependencies.
Regards,
Randall
Quoted 6 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) and commits/tags can be signed using the private key from your ssh-agent.
Quoted 7 lines
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.
Signature · 1 line
An old man doll... just what I always wanted! - Clara
Quoted 12 lines · Bagas Sanjaya
Quoted 11 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keysfile) 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 6 lines
Quoted 2 lines
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.
PATCH v2 1/1 Add commit, tag & push signing/verification via SSH keys using ssh-keygen
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
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.
Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) "ALLOWED SIGNERS") which contains valid public keys and an 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.
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.
---
RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
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.
There is a patch in the pipeline for openssh by Damien Miller that 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
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v2
Pull-Request: https://github.com/git/git/pull/1041
Range-diff vs v1:
1: f238392bfa8 ! 1: b8b16f8e6ec Add commit & tag signing/verification via SSH keys using ssh-keygen
@@ Metadata
Author: Fabian Stelzer <[email protected]>
## Commit message ##
- Add commit & tag signing/verification via SSH keys using ssh-keygen
+ Add commit, tag & push signing/verification via SSH keys using ssh-keygen
- 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.
+ 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.
- 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.
+ 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.
- needs openssh>8.2p1
+ Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) "ALLOWED
+ SIGNERS") which contains valid public keys and an 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.
+
+ 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 <[email protected]>
@@ Documentation/config/gpg.txt: gpg.program::
gpg.minTrustLevel::
Specifies a minimum trust level for signature verification. If
@@ Documentation/config/gpg.txt: 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:
+ * `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.
+
-+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
++ 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.
++ 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.
- +
- * `undefined`
- * `never`
++ 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.
## Documentation/config/user.txt ##
@@ Documentation/config/user.txt: 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.
++ 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.
+
+ ## builtin/receive-pack.c ##
+@@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const char *value, void *cb)
+ {
+ int status = parse_hide_refs_config(var, value, "receive");
+
++ git_gpg_config(var, value, NULL);
++
+ if (status)
+ return status;
+
+@@ builtin/receive-pack.c: static void prepare_push_cert_sha1(struct child_process *proc)
+ bogs = parse_signed_buffer(push_cert.buf, push_cert.len);
+ check_signature(push_cert.buf, bogs, push_cert.buf + bogs,
+ push_cert.len - bogs, &sigcheck);
+-
++
+ nonce_status = check_nonce(push_cert.buf, bogs);
+ }
+ if (!is_null_oid(&push_cert_oid)) {
+
+ ## fmt-merge-msg.c ##
+@@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)
+ len = payload.len;
+ if (check_signature(payload.buf, payload.len, sig.buf,
+ sig.len, &sigc) &&
+- !sigc.gpg_output)
++ !sigc.output)
+ strbuf_addstr(&sig, "gpg verification failed.\n");
+ else
+- strbuf_addstr(&sig, sigc.gpg_output);
++ strbuf_addstr(&sig, sigc.output);
+ }
+ signature_check_clear(&sigc);
+
## gpg-interface.c ##
@@
+ #include "config.h"
+ #include "run-command.h"
+ #include "strbuf.h"
++#include "dir.h"
+ #include "gpg-interface.h"
+ #include "sigchain.h"
#include "tempfile.h"
static char *configured_signing_key;
@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
};
static struct gpg_format *use_format = &gpg_format[0];
+@@ gpg-interface.c: static struct gpg_format *get_format_by_sig(const char *sig)
+ void signature_check_clear(struct signature_check *sigc)
+ {
+ FREE_AND_NULL(sigc->payload);
++ FREE_AND_NULL(sigc->output);
+ FREE_AND_NULL(sigc->gpg_output);
+ FREE_AND_NULL(sigc->gpg_status);
+ FREE_AND_NULL(sigc->signer);
@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,
return 1;
}
@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,
+ const char *output = NULL;
+ char *next = NULL;
+
-+ // ssh-keysign output should be:
-+ // Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
++ /* 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
++ */
+
-+ output = xmemdupz(sigc->gpg_status, strcspn(sigc->gpg_status, " \n"));
-+ if (skip_prefix(sigc->gpg_status, "Good \"git\" signature for ", &output)) {
++ output = xmemdupz(sigc->output, strcspn(sigc->output, " \n"));
++ if (skip_prefix(sigc->output, "Good \"git\" signature for ", &output)) {
++ // Valid signature for a trusted signer
+ sigc->result = 'G';
++ sigc->trust_level = TRUST_FULLY;
+
-+ next = strchrnul(output, ' ');
++ next = strchrnul(output, ' '); // 'principal'
+ replace_cstring(&sigc->signer, output, next);
+ output = next + 1;
+ next = strchrnul(output, ' '); // 'with'
@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,
+ output = next + 1;
+ next = strchrnul(output, ' '); // 'key'
+ output = next + 1;
-+ next = strchrnul(output, ' '); // key
++ next = strchrnul(output, '\n'); // key
+ replace_cstring(&sigc->fingerprint, output, next);
++ replace_cstring(&sigc->key, output, next);
++ } else if (skip_prefix(sigc->output, "Good \"git\" signature with ", &output)) {
++ // Valid signature, but key unknown
++ sigc->result = 'G';
++ sigc->trust_level = TRUST_UNDEFINED;
++
++ next = strchrnul(output, ' '); // KEY Type
++ output = next + 1;
++ next = strchrnul(output, ' '); // 'key'
++ output = next + 1;
++ next = strchrnul(output, '\n'); // key
++ replace_cstring(&sigc->fingerprint, output, next);
++ replace_cstring(&sigc->key, output, next);
+ } else {
+ sigc->result = 'B';
++ sigc->trust_level = TRUST_NEVER;
+ }
-+
-+ // 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));
+}
+
static void parse_gpg_output(struct signature_check *sigc)
{
const char *buf = sigc->gpg_status;
-@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,
- struct strbuf *gpg_output,
- struct strbuf *gpg_status)
+@@ gpg-interface.c: error:
+ FREE_AND_NULL(sigc->key);
+ }
+
+-static int verify_signed_buffer(const char *payload, size_t payload_size,
+- const char *signature, size_t signature_size,
+- struct strbuf *gpg_output,
+- struct strbuf *gpg_status)
++static int verify_ssh_signature(struct signature_check *sigc, struct gpg_format *fmt,
++ const char *payload, size_t payload_size,
++ const char *signature, size_t signature_size)
{
- struct child_process gpg = CHILD_PROCESS_INIT;
-+ struct child_process gpg = CHILD_PROCESS_INIT,
-+ ssh_keygen = CHILD_PROCESS_INIT;
- struct gpg_format *fmt;
+- struct gpg_format *fmt;
++ struct child_process ssh_keygen = CHILD_PROCESS_INIT;
struct tempfile *temp;
int ret;
- struct strbuf buf = STRBUF_INIT;
+ const char *line;
+ size_t trust_size;
+ char *principal;
-+ struct strbuf buf = STRBUF_INIT,
-+ principal_out = STRBUF_INIT,
-+ principal_err = STRBUF_INIT;
++ struct strbuf ssh_keygen_out = STRBUF_INIT;
++ struct strbuf ssh_keygen_err = STRBUF_INIT;
temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
if (!temp)
@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,
- if (!fmt)
- BUG("bad signature '%s'", signature);
+ 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 (!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(),
+- 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, 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)
-+ goto out;
-+
-+ /* Iterate over all lines */
-+ for (line = principal_out.buf; *line; line = strchrnul(line + 1, '\n')) {
++ 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;
-
-- 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);
++
++ 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(&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);
-+ }
++ 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 (!gpg_status)
-+ gpg_status = &buf;
++ 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(&gpg, payload, payload_size,
-+ gpg_status, 0, gpg_output, 0);
++ ret = pipe_command(&ssh_keygen, payload, payload_size,
++ &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+ sigchain_pop(SIGPIPE);
-
-- ret |= !strstr(gpg_status->buf, "\n[GNUPG:] GOODSIG ");
-+ ret |= !strstr(gpg_status->buf, "Good");
++
++ ret &= starts_with(ssh_keygen_out.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;
++ 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);
+
-+ 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 ");
++ //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);
++
++ return ret;
++}
++
++static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,
++ const char *payload, size_t payload_size,
++ const char *signature, size_t signature_size)
++{
++ struct child_process gpg = CHILD_PROCESS_INIT;
++ struct tempfile *temp;
++ int ret;
++ struct strbuf gpg_out = STRBUF_INIT;
++ struct strbuf gpg_err = STRBUF_INIT;
++
++ temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
++ if (!temp)
++ return error_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);
+
-+out:
+ delete_tempfile(&temp);
-+ strbuf_release(&principal_out);
-+ strbuf_release(&principal_err);
- strbuf_release(&buf); /* no matter it was used or not */
++ strbuf_release(&gpg_out);
++ strbuf_release(&gpg_err);
return ret;
-@@ gpg-interface.c: int check_signature(const char *payload, size_t plen, const char *signature,
- sigc->payload = xmemdupz(payload, plen);
- sigc->gpg_output = strbuf_detach(&gpg_output, NULL);
- sigc->gpg_status = strbuf_detach(&gpg_status, NULL);
+ }
+@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,
+ int check_signature(const char *payload, size_t plen, const char *signature,
+ size_t slen, struct signature_check *sigc)
+ {
+- struct strbuf gpg_output = STRBUF_INIT;
+- struct strbuf gpg_status = STRBUF_INIT;
++ struct gpg_format *fmt;
+ int status;
+
+ sigc->result = 'N';
+ sigc->trust_level = -1;
+
+- status = verify_signed_buffer(payload, plen, signature, slen,
+- &gpg_output, &gpg_status);
+- if (status && !gpg_output.len)
+- goto out;
+- sigc->payload = xmemdupz(payload, plen);
+- sigc->gpg_output = strbuf_detach(&gpg_output, NULL);
+- sigc->gpg_status = strbuf_detach(&gpg_status, NULL);
- parse_gpg_output(sigc);
-+ if (!strcmp(use_format->name, "ssh")) {
-+ parse_ssh_output(sigc);
++ fmt = get_format_by_sig(signature);
++ if (!fmt)
++ BUG("bad signature '%s'", signature);
++
++ if (!strcmp(fmt->name, "ssh")) {
++ status = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);
+ } else {
-+ parse_gpg_output(sigc);
++ status = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);
+ }
++ if (status && !sigc->gpg_output)
++ return !!status;
++
status |= sigc->result != 'G';
status |= sigc->trust_level < configured_min_trust_level;
+- out:
+- strbuf_release(&gpg_status);
+- strbuf_release(&gpg_output);
+-
+ return !!status;
+ }
+
+ void print_signature_buffer(const struct signature_check *sigc, unsigned flags)
+ {
+ const char *output = flags & GPG_VERIFY_RAW ?
+- sigc->gpg_status : sigc->gpg_output;
++ sigc->gpg_status : sigc->output;
+
+ if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
+ fputs(sigc->payload, stdout);
@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
+ int ret;
+
+ if (!strcmp(var, "user.signingkey")) {
++ /* user.signingkey can contain one of the following
++ * when format = openpgp/x509
++ * - GPG KeyID
++ * when format = ssh
++ * - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)
++ * - path to a file containing a public or a private ssh key
++ */
+ if (!value)
+ return config_error_nonbool(var);
+ set_signing_key(value);
return 0;
}
-+ if (!strcmp(var, "gpg.ssh.allowedsigners")) {
++ if (!strcmp(var, "gpg.ssh.keyring")) {
++ if (!value)
++ return config_error_nonbool(var);
+ return git_config_string(&ssh_allowed_signers, var, value);
+ }
+
-+ if (!strcmp(var, "gpg.ssh.revocationfile")) {
++ if (!strcmp(var, "gpg.ssh.revocationkeyring")) {
++ if (!value)
++ return config_error_nonbool(var);
+ return git_config_string(&ssh_revocation_file, var, value);
+ }
+
@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
if (fmtname) {
fmt = get_format_by_name(fmtname);
return git_config_string(&fmt->program, var, value);
-@@ gpg-interface.c: const char *get_signing_key(void)
+@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
+ return 0;
+ }
+
++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);
++}
++
++// 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);
++ }
++
++ return "";
++}
++
++// Returns a textual but unique representation ot the signing key
++const char *get_signing_key_id(void) {
++ if (!strcmp(use_format->name, "ssh")) {
++ return get_ssh_key_fingerprint(get_signing_key());
++ } else {
++ // GPG/GPGSM only store a key id on this variable
++ return get_signing_key();
++ }
++}
++
+ const char *get_signing_key(void)
{
if (configured_signing_key)
return configured_signing_key;
- return git_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 "";
++ return get_default_ssh_signing_key();
+ } else {
+ return git_committer_info(IDENT_STRICT | IDENT_NO_DATE);
+ }
@@ gpg-interface.c: const char *get_signing_key(void)
+{
+ if (ssh_allowed_signers)
+ return ssh_allowed_signers;
-+ return GPG_SSH_ALLOWED_SIGNERS;
++
++ die("A Path to an allowed signers ssh keyring is needed for validation");
}
int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature
int ret;
size_t i, j, bottom;
struct strbuf gpg_status = STRBUF_INIT;
--
++ struct tempfile *temp = NULL, *buffer_file = NULL;
++ char *ssh_signing_key_file = NULL;
++ struct strbuf ssh_signature_filename = STRBUF_INIT;
++
++ if (!strcmp(use_format->name, "ssh")) {
++ if (!signing_key || signing_key[0] == '\0')
++ return error(_("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)
++ return error_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);
-+ struct tempfile *temp = NULL;
-+
-+ if (!strcmp(use_format->name, "ssh")) {
-+ if (!signing_key)
-+ return error(_("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)
++ buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
++ if (!buffer_file)
+ return error_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);
++ 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", temp->filename.buf,
++ "-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);
++
++ /*
++ * When the username signingkey is bad, program could be terminated
++ * because gpg exits without reading and then write gets SIGPIPE.
++ */
++ sigchain_push(SIGPIPE, SIG_IGN);
++ ret = pipe_command(&gpg, buffer->buf, buffer->len, signature, 1024, &gpg_status, 0);
++ sigchain_pop(SIGPIPE);
+ }
bottom = signature->len;
-@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
- signature, 1024, &gpg_status, 0);
- sigchain_pop(SIGPIPE);
-
-- ret |= !strstr(gpg_status.buf, "\n[GNUPG:] SIG_CREATED ");
+- /*
+- * When the username signingkey is bad, program could be terminated
+- * because gpg exits without reading and then write gets SIGPIPE.
+- */
+- 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)"));
@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature
return error(_("gpg failed to sign the data"));
## gpg-interface.h ##
-@@ gpg-interface.h: struct strbuf;
- #define GPG_VERIFY_RAW 2
- #define GPG_VERIFY_OMIT_STATUS 4
+@@ gpg-interface.h: enum signature_trust_level {
-+#define GPG_SSH_ALLOWED_SIGNERS ".gitsigners"
-+
- enum signature_trust_level {
- TRUST_UNDEFINED,
- TRUST_NEVER,
+ struct signature_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":
@@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature,
int git_gpg_config(const char *, const char *, void *);
void set_signing_key(const char *);
const char *get_signing_key(void);
++
++/* Returns a textual unique representation of the signing key in use
++ * Either a GPG KeyID or a SSH Key Fingerprint
++ */
++const char *get_signing_key_id(void);
++
+const char *get_ssh_allowed_signers(void);
int check_signature(const char *payload, size_t plen,
const char *signature, size_t slen,
struct signature_check *sigc);
+
+ ## log-tree.c ##
+@@ log-tree.c: static void show_signature(struct rev_info *opt, struct commit *commit)
+
+ status = check_signature(payload.buf, payload.len, signature.buf,
+ signature.len, &sigc);
+- if (status && !sigc.gpg_output)
++ if (status && !sigc.output)
+ show_sig_lines(opt, status, "No signature\n");
+ else
+- show_sig_lines(opt, status, sigc.gpg_output);
++ show_sig_lines(opt, status, sigc.output);
+ signature_check_clear(&sigc);
+
+ out:
+@@ log-tree.c: 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);
+ else
+ strbuf_addstr(&verify_message, "No signature\n");
+ signature_check_clear(&sigc);
+
+ ## pretty.c ##
+@@ pretty.c: static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
+ check_commit_signature(c->commit, &(c->signature_check));
+ switch (placeholder[1]) {
+ case 'G':
+- if (c->signature_check.gpg_output)
+- strbuf_addstr(sb, c->signature_check.gpg_output);
++ if (c->signature_check.output)
++ strbuf_addstr(sb, c->signature_check.output);
+ break;
+ case '?':
+ switch (c->signature_check.result) {
+
+ ## send-pack.c ##
+@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,
+ const struct ref *ref;
+ struct string_list_item *item;
+ char *signing_key = xstrdup(get_signing_key());
++ char *signing_key_id = xstrdup(get_signing_key_id());
+ const char *cp, *np;
+ struct strbuf cert = STRBUF_INIT;
+ int update_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) {
+@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,
+
+ free_return:
+ free(signing_key);
++ free(signing_key_id);
+ strbuf_release(&cert);
+ return update_seen;
+ }
+
+ ## t/lib-gpg.sh ##
+@@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
+ echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null
+ '
+
++test_lazy_prereq GPGSSH '
++ ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1)
++ test $? != 127 || exit 1
++ echo $ssh_version | grep -q "find-principals:missing signature file"
++ 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_\" NR \" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.keyring" &&
++ cat "${GNUPGHOME}/ssh.all_valid.keyring" &&
++ ssh-keygen -t ed25519 -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_KEYRING="${GNUPGHOME}/ssh.all_valid.keyring"
++
++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"
++
+ sanitize_pgp() {
+ perl -ne '
+ /^-----END PGP/ and $in_pgp = 0;
+
+ ## t/t4202-log.sh ##
+@@ t/t4202-log.sh: test_expect_success GPGSM 'setup signed branch x509' '
+ git commit -S -m signed_commit
+ '
+
++test_expect_success GPGSSH 'setup sshkey signed branch' '
++ test_config gpg.format ssh &&
++ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
++ test_when_finished "git reset --hard && git checkout main" &&
++ git checkout -b signed-ssh main &&
++ echo foo >foo &&
++ git add foo &&
++ git commit -S -m signed_commit
++'
++
+ test_expect_success GPGSM 'log x509 fingerprint' '
+ echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect &&
+ git log -n1 --format="%GF | %GP" signed-x509 >actual &&
+@@ t/t4202-log.sh: test_expect_success GPGSM 'log --graph --show-signature x509' '
+ grep "^| gpgsm: Good signature" actual
+ '
+
++test_expect_success GPGSSH 'log ssh key fingerprint' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect &&
++ git log -n1 --format="%GF | %GP" signed-ssh >actual &&
++ test_cmp expect actual
++'
++
+ test_expect_success GPG 'log --graph --show-signature for merged tag' '
+ test_when_finished "git reset --hard && git checkout main" &&
+ git checkout -b plain main &&
+
+ ## t/t5534-push-signed.sh ##
+@@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certificate' '
+ test_cmp expect dst/push-cert-status
+ '
+
++test_expect_success GPGSSH 'ssh signed push sends push certificate' '
++ prepare_dst &&
++ mkdir -p dst/.git/hooks &&
++ git -C dst config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git -C dst config receive.certnonceseed sekrit &&
++ write_script dst/.git/hooks/post-receive <<-\EOF &&
++ # discard the update list
++ cat >/dev/null
++ # record the push certificate
++ if test -n "${GIT_PUSH_CERT-}"
++ then
++ git cat-file blob $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_config gpg.format ssh &&
++ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
++ FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") &&
++ git push --signed dst noop ff +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=$(git rev-parse noop) &&
++ ff=$(git rev-parse ff) &&
++ noff=$(git rev-parse noff) &&
++ grep "$noop $ff refs/heads/ff" dst/push-cert &&
++ grep "$noop $noff refs/heads/noff" dst/push-cert &&
++ test_cmp expect dst/push-cert-status
++'
++
+ test_expect_success GPG 'inconsistent push options in signed push not allowed' '
+ # First, invoke receive-pack with dummy input to obtain its preamble.
+ prepare_dst &&
+@@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '
+ test_cmp expect dst/push-cert-status
+ '
+
++test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '
++ test_config gpg.format ssh &&
++ prepare_dst &&
++ mkdir -p dst/.git/hooks &&
++ git -C dst config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git -C dst config receive.certnonceseed sekrit &&
++ write_script dst/.git/hooks/post-receive <<-\EOF &&
++ # discard the update list
++ cat >/dev/null
++ # record the push certificate
++ if test -n "${GIT_PUSH_CERT-}"
++ then
++ git cat-file blob $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_config user.email [email protected] &&
++ test_config gpg.format ssh &&
++
++ test_config user.signingkey "" &&
++ (
++ sane_unset GIT_COMMITTER_EMAIL &&
++ test_must_fail git push --signed dst noop ff +noff
++ ) &&
++ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
++ FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") &&
++ git push --signed dst noop ff +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=$(git rev-parse noop) &&
++ ff=$(git rev-parse ff) &&
++ noff=$(git rev-parse noff) &&
++ grep "$noop $ff refs/heads/ff" dst/push-cert &&
++ grep "$noop $noff refs/heads/noff" dst/push-cert &&
++ test_cmp expect dst/push-cert-status
++'
++
+ test_expect_success GPG 'failed atomic push does not execute GPG' '
+ prepare_dst &&
+ git -C dst config receive.certnonceseed sekrit &&
+
+ ## t/t7031-verify-tag-signed-ssh.sh (new) ##
+@@
++#!/bin/sh
++
++test_description='signed tag tests'
++GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
++export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
++
++. ./test-lib.sh
++. "$TEST_DIRECTORY/lib-gpg.sh"
++
++test_expect_success GPGSSH 'create signed tags ssh' '
++ test_when_finished "test_unconfig commit.gpgsign" &&
++ test_config gpg.format ssh &&
++ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
++
++ echo 1 >file && git add file &&
++ test_tick && git commit -m initial &&
++ git tag -s -m initial initial &&
++ git branch side &&
++
++ echo 2 >file && test_tick && git commit -a -m second &&
++ git tag -s -m second second &&
++
++ git checkout side &&
++ echo 3 >elif && git add elif &&
++ test_tick && git commit -m "third on side" &&
++
++ git checkout main &&
++ test_tick && git merge -S side &&
++ git tag -s -m merge merge &&
++
++ echo 4 >file && test_tick && git commit -a -S -m "fourth unsigned" &&
++ git tag -a -m fourth-unsigned fourth-unsigned &&
++
++ test_tick && git commit --amend -S -m "fourth signed" &&
++ git tag -s -m fourth fourth-signed &&
++
++ echo 5 >file && test_tick && git commit -a -m "fifth" &&
++ git tag fifth-unsigned &&
++
++ git config commit.gpgsign true &&
++ echo 6 >file && test_tick && git commit -a -m "sixth" &&
++ git tag -a -m sixth sixth-unsigned &&
++
++ test_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&
++ git tag -m seventh -s seventh-signed &&
++
++ echo 8 >file && test_tick && git commit -a -m eighth &&
++ git tag -u"${SIGNING_KEY_UNTRUSTED}" -m eighth eighth-signed-alt
++'
++
++test_expect_success GPGSSH 'verify and show ssh signatures' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.mintrustlevel UNDEFINED &&
++ (
++ for tag in initial second merge fourth-signed sixth-signed seventh-signed
++ do
++ git verify-tag $tag 2>actual &&
++ grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ echo $tag OK || exit 1
++ done
++ ) &&
++ (
++ for tag in fourth-unsigned fifth-unsigned sixth-unsigned
++ do
++ test_must_fail git verify-tag $tag 2>actual &&
++ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ echo $tag OK || exit 1
++ done
++ ) &&
++ (
++ for tag in eighth-signed-alt
++ do
++ git verify-tag $tag 2>actual &&
++ grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ grep "${KEY_NOT_TRUSTED}" actual &&
++ echo $tag OK || exit 1
++ done
++ )
++'
++
++test_expect_success GPGSSH 'detect fudged ssh signature' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git cat-file tag seventh-signed >raw &&
++ sed -e "/^tag / s/seventh/7th forged/" raw >forged1 &&
++ git hash-object -w -t tag forged1 >forged1.tag &&
++ test_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&
++ grep "${BAD_SIGNATURE}" actual1 &&
++ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual1 &&
++ ! grep "${GOOD_SIGNATURE_UNTRUSTED}" actual1
++'
++
++# test_expect_success GPGSSH 'verify ssh signatures with --raw' '
++# test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++# (
++# for tag in initial second merge fourth-signed sixth-signed seventh-signed
++# do
++# git verify-tag --raw $tag 2>actual &&
++# grep "GOODSIG" actual &&
++# ! grep "BADSIG" actual &&
++# echo $tag OK || exit 1
++# done
++# ) &&
++# (
++# for tag in fourth-unsigned fifth-unsigned sixth-unsigned
++# do
++# test_must_fail git verify-tag --raw $tag 2>actual &&
++# ! grep "GOODSIG" actual &&
++# ! grep "BADSIG" actual &&
++# echo $tag OK || exit 1
++# done
++# ) &&
++# (
++# for tag in eighth-signed-alt
++# do
++# git verify-tag --raw $tag 2>actual &&
++# grep "GOODSIG" actual &&
++# ! grep "BADSIG" actual &&
++# grep "TRUST_UNDEFINED" actual &&
++# echo $tag OK || exit 1
++# done
++# )
++# '
++
++# test_expect_success GPGSM 'verify signatures with --raw x509' '
++# git verify-tag --raw ninth-signed-x509 2>actual &&
++# grep "GOODSIG" actual &&
++# ! grep "BADSIG" actual &&
++# echo ninth-signed-x509 OK
++# '
++
++# test_expect_success GPGSSH 'verify multiple tags' '
++# test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++# tags="fourth-signed sixth-signed seventh-signed" &&
++# for i in $tags
++# do
++# git verify-tag -v --raw $i || return 1
++# done >expect.stdout 2>expect.stderr.1 &&
++# grep "^.GNUPG:." <expect.stderr.1 >expect.stderr &&
++# git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
++# grep "^.GNUPG:." <actual.stderr.1 >actual.stderr &&
++# test_cmp expect.stdout actual.stdout &&
++# test_cmp expect.stderr actual.stderr
++# '
++
++# test_expect_success GPGSM 'verify multiple tags x509' '
++# test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++# tags="seventh-signed ninth-signed-x509" &&
++# for i in $tags
++# do
++# git verify-tag -v --raw $i || return 1
++# done >expect.stdout 2>expect.stderr.1 &&
++# grep "^.GNUPG:." <expect.stderr.1 >expect.stderr &&
++# git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
++# grep "^.GNUPG:." <actual.stderr.1 >actual.stderr &&
++# test_cmp expect.stdout actual.stdout &&
++# test_cmp expect.stderr actual.stderr
++# '
++
++test_expect_success GPGSSH 'verifying tag with --format' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ cat >expect <<-\EOF &&
++ tagname : fourth-signed
++ EOF
++ git verify-tag --format="tagname : %(tag)" "fourth-signed" >actual &&
++ test_cmp expect actual
++'
++
++test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently' '
++ test_must_fail git verify-tag --format="tagname : %(tag)" $(cat forged1.tag) >actual-forged &&
++ test_must_be_empty actual-forged
++'
++
++test_done
+
+ ## t/t7527-signed-commit-ssh.sh (new) ##
+@@
++#!/bin/sh
++
++test_description='ssh signed commit tests'
++GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
++export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
++
++. ./test-lib.sh
++GNUPGHOME_NOT_USED=$GNUPGHOME
++. "$TEST_DIRECTORY/lib-gpg.sh"
++
++test_expect_success GPGSSH 'create signed commits' '
++ test_oid_cache <<-\EOF &&
++ header sha1:gpgsig
++ header sha256:gpgsig-sha256
++ EOF
++
++ test_when_finished "test_unconfig commit.gpgsign" &&
++ test_config gpg.format ssh &&
++ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
++
++ echo 1 >file && git add file &&
++ test_tick && git commit -S -m initial &&
++ git tag initial &&
++ git branch side &&
++
++ echo 2 >file && test_tick && git commit -a -S -m second &&
++ git tag second &&
++
++ git checkout side &&
++ echo 3 >elif && git add elif &&
++ test_tick && git commit -m "third on side" &&
++
++ git checkout main &&
++ test_tick && git merge -S side &&
++ git tag merge &&
++
++ echo 4 >file && test_tick && git commit -a -m "fourth unsigned" &&
++ git tag fourth-unsigned &&
++
++ test_tick && git commit --amend -S -m "fourth signed" &&
++ git tag fourth-signed &&
++
++ git config commit.gpgsign true &&
++ echo 5 >file && test_tick && git commit -a -m "fifth signed" &&
++ git tag fifth-signed &&
++
++ git config commit.gpgsign false &&
++ echo 6 >file && test_tick && git commit -a -m "sixth" &&
++ git tag sixth-unsigned &&
++
++ git config commit.gpgsign true &&
++ echo 7 >file && test_tick && git commit -a -m "seventh" --no-gpg-sign &&
++ git tag seventh-unsigned &&
++
++ test_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ &&
++ git tag seventh-signed &&
++
++ echo 8 >file && test_tick && git commit -a -m eighth -S"${SIGNING_KEY_UNTRUSTED}" &&
++ git tag eighth-signed-alt &&
++
++ # commit.gpgsign is still on but this must not be signed
++ echo 9 | git commit-tree HEAD^{tree} >oid &&
++ test_line_count = 1 oid &&
++ git tag ninth-unsigned $(cat oid) &&
++ # explicit -S of course must sign.
++ echo 10 | git commit-tree -S HEAD^{tree} >oid &&
++ test_line_count = 1 oid &&
++ git tag tenth-signed $(cat oid) &&
++
++ # --gpg-sign[=<key-id>] must sign.
++ echo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid &&
++ test_line_count = 1 oid &&
++ git tag eleventh-signed $(cat oid) &&
++ echo 12 | git commit-tree --gpg-sign="${SIGNING_KEY_UNTRUSTED}" HEAD^{tree} >oid &&
++ test_line_count = 1 oid &&
++ git tag twelfth-signed-alt $(cat oid)
++'
++
++test_expect_success GPGSSH 'verify and show signatures' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.mintrustlevel UNDEFINED &&
++ (
++ for commit in initial second merge fourth-signed \
++ fifth-signed sixth-signed seventh-signed tenth-signed \
++ eleventh-signed
++ do
++ git verify-commit $commit &&
++ git show --pretty=short --show-signature $commit >actual &&
++ grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ echo $commit OK || exit 1
++ done
++ ) &&
++ (
++ for commit in merge^2 fourth-unsigned sixth-unsigned \
++ seventh-unsigned ninth-unsigned
++ do
++ test_must_fail git verify-commit $commit &&
++ git show --pretty=short --show-signature $commit >actual &&
++ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ echo $commit OK || exit 1
++ done
++ ) &&
++ (
++ for commit in eighth-signed-alt twelfth-signed-alt
++ do
++ git show --pretty=short --show-signature $commit >actual &&
++ grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ grep "${KEY_NOT_TRUSTED}" actual &&
++ echo $commit OK || exit 1
++ done
++ )
++'
++
++test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git verify-commit eighth-signed-alt 2>actual &&
++ grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ grep "${KEY_NOT_TRUSTED}" actual
++'
++
++test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.minTrustLevel fully &&
++ git verify-commit sixth-signed
++'
++
++test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.minTrustLevel marginal &&
++ git verify-commit sixth-signed
++'
++
++test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' '
++ test_config gpg.minTrustLevel ultimate &&
++ test_must_fail git verify-commit eighth-signed-alt
++'
++
++# test_expect_success GPGSSH 'verify signatures with --raw' '
++# (
++# for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
++# do
++# git verify-commit --raw $commit 2>actual &&
++# grep "GOODSIG" actual &&
++# ! grep "BADSIG" actual &&
++# echo $commit OK || exit 1
++# done
++# ) &&
++# (
++# for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned
++# do
++# test_must_fail git verify-commit --raw $commit 2>actual &&
++# ! grep "GOODSIG" actual &&
++# ! grep "BADSIG" actual &&
++# echo $commit OK || exit 1
++# done
++# ) &&
++# (
++# for commit in eighth-signed-alt
++# do
++# git verify-commit --raw $commit 2>actual &&
++# grep "GOODSIG" actual &&
++# ! grep "BADSIG" actual &&
++# grep "TRUST_UNDEFINED" actual &&
++# echo $commit OK || exit 1
++# done
++# )
++# '
++
++test_expect_success GPGSSH 'proper header is used for hash algorithm' '
++ git cat-file commit fourth-signed >output &&
++ grep "^$(test_oid header) -----BEGIN SSH SIGNATURE-----" output
++'
++
++test_expect_success GPGSSH 'show signed commit with signature' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git show -s initial >commit &&
++ git show -s --show-signature initial >show &&
++ git verify-commit -v initial >verify.1 2>verify.2 &&
++ git cat-file commit initial >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_cmp show.commit commit &&
++ test_cmp show.gpg verify.2 &&
++ test_cmp cat.commit verify.1
++'
++
++test_expect_success GPGSSH 'detect fudged signature' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git cat-file commit seventh-signed >raw &&
++ sed -e "s/^seventh/7th forged/" raw >forged1 &&
++ git hash-object -w -t commit forged1 >forged1.commit &&
++ test_must_fail git verify-commit $(cat forged1.commit) &&
++ git show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&
++ grep "${BAD_SIGNATURE}" actual1 &&
++ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual1 &&
++ ! grep "${GOOD_SIGNATURE_UNTRUSTED}" actual1
++'
++
++test_expect_success GPGSSH 'detect fudged signature with NUL' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git cat-file commit seventh-signed >raw &&
++ cat raw >forged2 &&
++ echo Qwik | tr "Q" "\000" >>forged2 &&
++ git hash-object -w -t commit forged2 >forged2.commit &&
++ test_must_fail git verify-commit $(cat forged2.commit) &&
++ git show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&
++ grep "${BAD_SIGNATURE}" actual2 &&
++ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual2
++'
++
++test_expect_success GPGSSH 'amending already signed commit' '
++ test_config gpg.format ssh &&
++ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git checkout fourth-signed^0 &&
++ git commit --amend -S --no-edit &&
++ git verify-commit HEAD &&
++ git show -s --show-signature HEAD >actual &&
++ grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual
++'
++
++test_expect_success GPGSSH 'show good signature with custom format' '
++ test_config gpg.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 &&
++ git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual &&
++ test_cmp expect actual
++'
++
++test_expect_success GPGSSH 'show bad signature with custom format' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ cat >expect <<-\EOF &&
++ B
++
++
++
++
++ EOF
++ git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat forged1.commit) >actual &&
++ test_cmp expect actual
++'
++
++test_expect_success GPGSSH 'show untrusted signature with custom format' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ cat >expect.tmpl <<-\EOF &&
++ U
++ FINGERPRINT
++
++ FINGERPRINT
++
++ EOF
++ git log -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_cmp expect actual
++'
++
++test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ cat >expect.tmpl <<-\EOF &&
++ undefined
++ FINGERPRINT
++
++ FINGERPRINT
++
++ EOF
++ git log -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_cmp expect actual
++'
++
++test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ cat >expect.tmpl <<-\EOF &&
++ fully
++ FINGERPRINT
++ principal_1
++ FINGERPRINT
++
++ EOF
++ git log -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_cmp expect actual
++'
++
++test_expect_success GPGSSH 'show lack of signature with custom format' '
++ cat >expect <<-\EOF &&
++ N
++
++
++
++
++ EOF
++ git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" seventh-unsigned >actual &&
++ test_cmp expect actual
++'
++
++test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config log.showsignature true &&
++ git show initial >actual &&
++ grep "${GOOD_SIGNATURE_TRUSTED}" actual
++'
++
++test_expect_success GPGSSH 'check config gpg.format values' '
++ test_config gpg.format ssh &&
++ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
++ test_config gpg.format ssh &&
++ git commit -S --amend -m "success" &&
++ test_config gpg.format OpEnPgP &&
++ test_must_fail git commit -S --amend -m "fail"
++'
++
++# test_expect_success GPGSSH 'detect fudged commit with double signature' '
++# sed -e "/gpgsig/,/END PGP/d" forged1 >double-base &&
++# sed -n -e "/gpgsig/,/END PGP/p" forged1 | \
++# sed -e "s/^$(test_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig &&
++# gpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&
++# cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&
++# sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \
++# double-combined.asc > double-gpgsig &&
++# sed -e "/committer/r double-gpgsig" double-base >double-commit &&
++# git hash-object -w -t commit double-commit >double-commit.commit &&
++# test_must_fail git verify-commit $(cat double-commit.commit) &&
++# git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&
++# grep "BAD signature from" double-actual &&
++# grep "Good signature from" double-actual
++# '
++
++# test_expect_success GPGSSH 'show double signature with custom format' '
++# cat >expect <<-\EOF &&
++# E
++
++
++
++
++# EOF
++# git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual &&
++# test_cmp expect actual
++# '
++
++
++# test_expect_success GPGSSH 'verify-commit verifies multiply signed commits' '
++# git init multiply-signed &&
++# cd multiply-signed &&
++# test_commit first &&
++# echo 1 >second &&
++# git add second &&
++# tree=$(git write-tree) &&
++# parent=$(git rev-parse HEAD^{commit}) &&
++# git commit --gpg-sign -m second &&
++# git cat-file commit HEAD &&
++# # Avoid trailing whitespace.
++# sed -e "s/^Q//" -e "s/^Z/ /" >commit <<-EOF &&
++# Qtree $tree
++# Qparent $parent
++# Qauthor A U Thor <[email protected]> 1112912653 -0700
++# Qcommitter C O Mitter <[email protected]> 1112912653 -0700
++# Qgpgsig -----BEGIN PGP SIGNATURE-----
++# QZ
++# Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy
++# Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC
++# Q AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==
++# Q =tQ0N
++# Q -----END PGP SIGNATURE-----
++# Qgpgsig-sha256 -----BEGIN PGP SIGNATURE-----
++# QZ
++# Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy
++# Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO
++# Q AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==
++# Q =pIwP
++# Q -----END PGP SIGNATURE-----
++# Q
++# Qsecond
++# EOF
++# head=$(git hash-object -t commit -w commit) &&
++# git reset --hard $head &&
++# git verify-commit $head 2>actual &&
++# grep "Good signature from" actual &&
++# ! grep "BAD signature from" actual
++# '
++
++test_doneDocumentation/config/gpg.txt | 35 ++- Documentation/config/user.txt | 6 + builtin/receive-pack.c | 4 +- fmt-merge-msg.c | 4 +- gpg-interface.c | 414 +++++++++++++++++++++++++++---- gpg-interface.h | 12 +- log-tree.c | 8 +- pretty.c | 4 +- send-pack.c | 6 +- t/lib-gpg.sh | 27 ++ t/t4202-log.sh | 17 ++ t/t5534-push-signed.sh | 102 ++++++++ t/t7031-verify-tag-signed-ssh.sh | 176 +++++++++++++ t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++ 14 files changed, 1147 insertions(+), 66 deletions(-) create mode 100755 t/t7031-verify-tag-signed-ssh.sh create mode 100755 t/t7527-signed-commit-ssh.sh
Documentation/config/gpg.txt +33 -2
@@ -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.
Documentation/config/user.txt +6 -0
@@ -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.
builtin/receive-pack.c +3 -1
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) {int status =parse_hide_refs_config (var, value,"receive" ); +git_gpg_config (var, value,NULL ); +if (status)return status;
@@ -767,7 +769,7 @@ static void prepare_push_cert_sha1(struct child_process *proc) bogs =parse_signed_buffer (push_cert.buf, push_cert.len);check_signature (push_cert.buf, bogs, push_cert.buf + bogs, push_cert.len - bogs, &sigcheck); - + nonce_status =check_nonce (push_cert.buf, bogs); }if (!is_null_oid (&push_cert_oid)) {
fmt-merge-msg.c +2 -2
@@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out) len = payload.len;if (check_signature (payload.buf, payload.len, sig.buf, sig.len, &sigc) && - !sigc.gpg_output) + !sigc.output)strbuf_addstr (&sig,"gpg verification failed.\n" );else -strbuf_addstr (&sig, sigc.gpg_output); +strbuf_addstr (&sig, sigc.output); }signature_check_clear (&sigc);
gpg-interface.c +363 -51
532 lines not on this page · raw message
gpg-interface.h +10 -2
@@ -17,8 +17,9 @@ enum signature_trust_level {struct signature_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,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); + +/* Returns a textual unique representation of the signing key in use +* Either a GPG KeyID or a SSH Key Fingerprint +*/ +const char *get_signing_key_id (void ); + +const char *get_ssh_allowed_signers (void );int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc);
log-tree.c +4 -4
@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit) status =check_signature (payload.buf, payload.len, signature.buf, signature.len, &sigc); -if (status && !sigc.gpg_output) +if (status && !sigc.output)show_sig_lines (opt, status,"No signature\n" );else -show_sig_lines (opt, status, sigc.gpg_output); +show_sig_lines (opt, status, sigc.output);signature_check_clear (&sigc); out:
@@ -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);else strbuf_addstr (&verify_message,"No signature\n" );signature_check_clear (&sigc);
pretty.c +2 -2
@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */check_commit_signature (c->commit, &(c->signature_check));switch (placeholder[1 ]) {case 'G' : -if (c->signature_check.gpg_output) -strbuf_addstr (sb, c->signature_check.gpg_output); +if (c->signature_check.output) +strbuf_addstr (sb, c->signature_check.output);break ;case '?' :switch (c->signature_check.result) {
send-pack.c +4 -2
@@ -342,12 +342,13 @@ static int generate_push_cert(struct strbuf *req_buf,const struct ref *ref;struct string_list_item *item;char *signing_key =xstrdup (get_signing_key ()); +char *signing_key_id =xstrdup (get_signing_key_id ());const char *cp, *np;struct strbuf cert =STRBUF_INIT ;int update_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) {
@@ -387,6 +388,7 @@ static int generate_push_cert(struct strbuf *req_buf, free_return:free (signing_key); +free (signing_key_id);strbuf_release (&cert);return update_seen; }
t/lib-gpg.sh +27 -0
@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 ' echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null ' +test_lazy_prereq GPGSSH ' + ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1) + test $? != 127 || exit 1 + echo $ssh_version | grep -q "find-principals:missing signature file" + 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_\" NR \" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.keyring" && + cat "${GNUPGHOME}/ssh.all_valid.keyring" && + ssh-keygen -t ed25519 -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_KEYRING="${GNUPGHOME}/ssh.all_valid.keyring" + +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" + sanitize_pgp() { perl -ne ' /^-----END PGP/ and $in_pgp = 0;
t/t4202-log.sh +17 -0
@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' ' git commit -S -m signed_commit ' +test_expect_success GPGSSH 'setup sshkey signed branch' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_when_finished "git reset --hard && git checkout main" && + git checkout -b signed-ssh main && + echo foo >foo && + git add foo && + git commit -S -m signed_commit +' + test_expect_success GPGSM 'log x509 fingerprint' ' echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && git log -n1 --format="%GF | %GP" signed-x509 >actual &&
@@ -1640,6 +1650,13 @@ test_expect_success GPGSM 'log --graph --show-signature x509' ' grep "^| gpgsm: Good signature" actual ' +test_expect_success GPGSSH 'log ssh key fingerprint' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect && + git log -n1 --format="%GF | %GP" signed-ssh >actual && + test_cmp expect actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout main" && git checkout -b plain main &&
t/t5534-push-signed.sh +102 -0
114 lines not on this page · raw message
t/t7031-verify-tag-signed-ssh.sh new file +176 -0
176 lines not on this page · raw message
t/t7527-signed-commit-ssh.sh new file +398 -0
398 lines not on this page · raw message
base-commit: d486ca60a51c9cb1fe068803c3f540724e95e83a
Signature · 1 line
gitgitgadget
Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen
Quoted 77 lines · Fabian Stelzer via GitGitGadget
gpg.format::
Documentation/config/gpg.txt +33
-2Specifies 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.Documentation/config/user.txt +6
-0@@ -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.builtin/receive-pack.c +2
-0@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) {int status =parse_hide_refs_config (var, value,"receive" ); +git_gpg_config (var, value,NULL ); +if (status)return status;
builtin/receive-pack.c static void prepare_push_cert_sha1(struct child_process *proc) · line 772
Quoted 5 lines
builtin/receive-pack.c +1
-1@@ -767,7 +769,7 @@ static void prepare_push_cert_sha1(struct child_process *proc) bogs =parse_signed_buffer (push_cert.buf, push_cert.len);check_signature (push_cert.buf, bogs, push_cert.buf + bogs, push_cert.len - bogs, &sigcheck); - +
Stray whitespace change.
builtin/receive-pack.c static void prepare_push_cert_sha1(struct child_process *proc)
Quoted 10 lines
builtin/receive-pack.c +10
-0+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 [...]
builtin/receive-pack.c
Quoted 4 lines
builtin/receive-pack.c +4
-0+ + 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...).
builtin/receive-pack.c
Quoted 12 lines
builtin/receive-pack.c +12
-0+ 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...
Quoted 26 lines
builtin/receive-pack.c +21
-0+ next =strchrnul (output,'\n' );// key +replace_cstring (&sigc->fingerprint, output, next); +replace_cstring (&sigc->key, output, next); + }else if (skip_prefix (sigc->output,"Good \"git\" signature with " , &output)) { +// Valid signature, but key unknown + sigc->result ='G' ; + sigc->trust_level =TRUST_UNDEFINED ; + + next =strchrnul (output,' ' );// KEY Type + output = next +1 ; + next =strchrnul (output,' ' );// 'key' + output = next +1 ; + next =strchrnul (output,'\n' );// key +replace_cstring (&sigc->fingerprint, output, next); +replace_cstring (&sigc->key, output, next); + }else { + sigc->result ='B' ; + sigc->trust_level =TRUST_NEVER ; + } +} +static void parse_gpg_output (struct signature_check *sigc) {const char *buf = sigc->gpg_status;
builtin/receive-pack.c error: · line 321
Quoted 8 lines
builtin/receive-pack.c +1
-4@@ -257,16 +318,18 @@ error:FREE_AND_NULL (sigc->key); } -static int verify_signed_buffer (const char *payload,size_t payload_size, -const char *signature,size_t signature_size, -struct strbuf *gpg_output, -struct strbuf *gpg_status) +static int verify_ssh_signature(struct signature_check *sigc, struct gpg_format *fmt,
We usually wrap at 80 characters, so since you're wrapping anyway...
Quoted 36 lines
builtin/receive-pack.c +20
-6+ const char *payload, size_t payload_size, + const char *signature, size_t signature_size) { -struct child_process gpg =CHILD_PROCESS_INIT ; -struct gpg_format *fmt; + struct child_process ssh_keygen = CHILD_PROCESS_INIT;struct tempfile *temp;int ret; -struct strbuf buf =STRBUF_INIT ; +const char *line; +size_t trust_size; +char *principal; +struct strbuf ssh_keygen_out =STRBUF_INIT ; +struct strbuf ssh_keygen_err =STRBUF_INIT ; temp =mks_tempfile_t (".git_vtag_tmpXXXXXX" );if (!temp)@@ -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
builtin/receive-pack.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 366
Quoted 10 lines
builtin/receive-pack.c +10
-0+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)
Quoted 13 lines
builtin/receive-pack.c +13
-0+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
builtin/receive-pack.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 389
Quoted 10 lines
builtin/receive-pack.c +10
-0+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?
Quoted 10 lines
builtin/receive-pack.c +10
-0+ } + +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 ;
builtin/receive-pack.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 409
Quoted 10 lines
builtin/receive-pack.c +10
-0+ } + } + + 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...
Broken how? And why the commented-out code as part of the patch?
builtin/receive-pack.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 412
Quoted 11 lines
builtin/receive-pack.c +3
-8- status =verify_signed_buffer (payload, plen, signature, slen, - &gpg_output, &gpg_status); -if (status && !gpg_output.len) - goto out; - sigc->payload =xmemdupz (payload, plen); - sigc->gpg_output =strbuf_detach (&gpg_output,NULL ); - sigc->gpg_status =strbuf_detach (&gpg_status,NULL ); -parse_gpg_output (sigc); + fmt =get_format_by_sig (signature); +if (!fmt) +BUG ("bad signature '%s'" , signature);
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?
Quoted 18 lines
builtin/receive-pack.c +18
-0+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 );
builtin/receive-pack.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 440
Quoted 10 lines
builtin/receive-pack.c +10
-0+ 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]
Quoted 18 lines
[...]
builtin/receive-pack.c +17
-0+# test_expect_success GPGSSH 'detect fudged commit with double signature' ' +# sed -e "/gpgsig/,/END PGP/d" forged1 >double-base && +# sed -n -e "/gpgsig/,/END PGP/p" forged1 | \ +# sed -e "s/^$(test_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig && +# gpg -o double-sig2.sig -u 29472784 --detach-sign double-base && +# cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc && +# sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \ +# double-combined.asc > double-gpgsig && +# sed -e "/committer/r double-gpgsig" double-base >double-commit && +# git hash-object -w -t commit double-commit >double-commit.commit && +# test_must_fail git verify-commit $(cat double-commit.commit) && +# git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual && +# grep "BAD signature from" double-actual && +# grep "Good signature from" double-actual +# ' + +# test_expect_success GPGSSH 'show double signature with custom format' '
builtin/receive-pack.c
Quoted 10 lines
builtin/receive-pack.c +10
-0+# cat >expect <<-\EOF && +# E + + + + +# EOF +# git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual && +# test_cmp expect actual +# '
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.
Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen
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 7 lines
Quoted 7 lines
gpg-interface.c +7
-0+ 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'
Quoted 8 lines
gpg-interface.c static int parse_gpg_trust_level(const char *level, · line 186
Quoted 5 lines
gpg-interface.c +5
-0+ 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()orstring_list_split_in_place()orstrbuf_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.
Quoted 41 lines
Quoted 41 lines
gpg-interface.c +25
-6+ const char *payload, size_t payload_size, + const char *signature, size_t signature_size) { -struct child_process gpg =CHILD_PROCESS_INIT ; -struct gpg_format *fmt; + struct child_process ssh_keygen = CHILD_PROCESS_INIT;struct tempfile *temp;int ret; -struct strbuf buf =STRBUF_INIT ; +const char *line; +size_t trust_size; +char *principal; +struct strbuf ssh_keygen_out =STRBUF_INIT ; +struct strbuf ssh_keygen_err =STRBUF_INIT ; temp =mks_tempfile_t (".git_vtag_tmpXXXXXX" );if (!temp)@@ -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,
Quoted 8 lines
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 366
Quoted 5 lines
gpg-interface.c +5
-0+ 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 underlyingio_pumpAPI thatpipe_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 16 lines
Quoted 16 lines
gpg-interface.c +16
-0+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",
Quoted 8 lines
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 389
Quoted 7 lines
gpg-interface.c +7
-0+"-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?
I'll add a warning
Quoted 14 lines
Quoted 14 lines
gpg-interface.c +13
-0+ } + +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 ; + } + } +
Quoted 8 lines
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 409
Quoted 7 lines
gpg-interface.c +7
-0+ 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... Broken how? And why the commented-out code as part of the patch?
Sorry, i should have removed it. The original code assigned gpg's stdout to gpg_status and stdout to gpg_output which can be a bit confusing.
Quoted 15 lines
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 479
Quoted 12 lines
gpg-interface.c +3
-8- status =verify_signed_buffer (payload, plen, signature, slen, - &gpg_output, &gpg_status); -if (status && !gpg_output.len) - goto out; - sigc->payload =xmemdupz (payload, plen); - sigc->gpg_output =strbuf_detach (&gpg_output,NULL ); - sigc->gpg_status =strbuf_detach (&gpg_status,NULL ); -parse_gpg_output (sigc); + fmt =get_format_by_sig (signature); +if (!fmt) +BUG ("bad signature '%s'" , signature);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 aBUG(), 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 23 lines
Quoted 23 lines
gpg-interface.c +22
-0+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 );
Quoted 8 lines
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 643
Quoted 6 lines
gpg-interface.c +6
-0+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 todie_errno()?
I'll clean up the logic. Thanks
Quoted 24 lines
[I stopped reading mostly at this point]
Quoted 21 lines
[...]
t/t7527-signed-commit-ssh.sh new file +20
-0+# test_expect_success GPGSSH 'detect fudged commit with double signature' ' +# sed -e "/gpgsig/,/END PGP/d" forged1 >double-base && +# sed -n -e "/gpgsig/,/END PGP/p" forged1 | \ +# sed -e "s/^$(test_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig && +# gpg -o double-sig2.sig -u 29472784 --detach-sign double-base && +# cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc && +# sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \ +# double-combined.asc > double-gpgsig && +# sed -e "/committer/r double-gpgsig" double-base >double-commit && +# git hash-object -w -t commit double-commit >double-commit.commit && +# test_must_fail git verify-commit $(cat double-commit.commit) && +# git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual && +# grep "BAD signature from" double-actual && +# grep "Good signature from" double-actual +# ' + +# test_expect_success GPGSSH 'show double signature with custom format' ' +# cat >expect <<-\EOF && +# E +
Quoted 8 lines
t/t7527-signed-commit-ssh.sh line 355
Quoted 7 lines
t/t7527-signed-commit-ssh.sh new file +7
-0+ + + +# EOF +# git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual && +# test_cmp expect actual +# 'Perhaps you're looking for
test_expect_failurefor 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.
Quoted 3 lines
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!
Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen
Quoted 7 lines · Fabian Stelzer
Quoted 2 lines · Ævar Arnfjörð Bjarmason
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.
Quoted 4 lines
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.
Quoted 2 lines
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.
Signature · 1 line
Felipe Contreras
PATCH v3 2/9 ssh signing: add documentation
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- Documentation/config/gpg.txt | 35 +++++++++++++++++++++++++++++++++-- Documentation/config/user.txt | 6 ++++++ 2 files changed, 39 insertions(+), 2 deletions(-)
Documentation/config/gpg.txt +33 -2
@@ -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.
Documentation/config/user.txt +6 -0
@@ -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.
Signature · 1 line
gitgitgadget
PATCH v3 1/9 Add commit, tag & push signing via SSH keys
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- 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(-)
fmt-merge-msg.c +2 -2
@@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out) len = payload.len;if (check_signature (payload.buf, payload.len, sig.buf, sig.len, &sigc) && - !sigc.gpg_output) + !sigc.output)strbuf_addstr (&sig,"gpg verification failed.\n" );else -strbuf_addstr (&sig, sigc.gpg_output); +strbuf_addstr (&sig, sigc.output); }signature_check_clear (&sigc);
gpg-interface.c +84 -38
214 lines not on this page · raw message
gpg-interface.h +3 -2
@@ -17,8 +17,8 @@ enum signature_trust_level {struct signature_check {char *payload; -char *gpg_output; -char *gpg_status; +char *output; +char *gpg_status;/* Only used internally -> remove from this public api */ /* * possible "result":
@@ -64,6 +64,7 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); +const char *get_ssh_allowed_signers (void );int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc);
log-tree.c +4 -4
@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit) status =check_signature (payload.buf, payload.len, signature.buf, signature.len, &sigc); -if (status && !sigc.gpg_output) +if (status && !sigc.output)show_sig_lines (opt, status,"No signature\n" );else -show_sig_lines (opt, status, sigc.gpg_output); +show_sig_lines (opt, status, sigc.output);signature_check_clear (&sigc); out:
@@ -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);else strbuf_addstr (&verify_message,"No signature\n" );signature_check_clear (&sigc);
pretty.c +2 -2
@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */check_commit_signature (c->commit, &(c->signature_check));switch (placeholder[1 ]) {case 'G' : -if (c->signature_check.gpg_output) -strbuf_addstr (sb, c->signature_check.gpg_output); +if (c->signature_check.output) +strbuf_addstr (sb, c->signature_check.output);break ;case '?' :switch (c->signature_check.result) {
Signature · 1 line
gitgitgadget
v3 cover RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
In reply to Fabian Stelzer via GitGitGadgetI 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.
There is a patch in the pipeline for openssh by Damien Miller that 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
v3 addresses some issues & refactoring and splits the large commit into several smaller ones.
Fabian Stelzer (9):
Add commit, tag & push signing via SSH keys
ssh signing: add documentation
ssh signing: retrieve a default key from ssh-agent ssh signing: sign using either gpg or ssh keys
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
Documentation/config/gpg.txt | 35 ++- Documentation/config/user.txt | 6 + builtin/receive-pack.c | 2 + fmt-merge-msg.c | 4 +- gpg-interface.c | 411 +++++++++++++++++++++++++++---- gpg-interface.h | 12 +- log-tree.c | 8 +- pretty.c | 4 +- send-pack.c | 8 +- t/lib-gpg.sh | 27 ++ t/t4202-log.sh | 23 ++ t/t5534-push-signed.sh | 101 ++++++++ t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++++ t/t7527-signed-commit-ssh.sh | 398 ++++++++++++++++++++++++++++++ 14 files changed, 1136 insertions(+), 64 deletions(-) create mode 100755 t/t7031-verify-tag-signed-ssh.sh create mode 100755 t/t7527-signed-commit-ssh.sh
base-commit: d486ca60a51c9cb1fe068803c3f540724e95e83a
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v3
Pull-Request: https://github.com/git/git/pull/1041
Range-diff vs v2:
-: ----------- > 1: 390a8f816cd Add commit, tag & push signing via SSH keys
-: ----------- > 2: 2f8452f6570 ssh signing: add documentation
-: ----------- > 3: b84b2812470 ssh signing: retrieve a default key from ssh-agent
-: ----------- > 4: df55b9e1d59 ssh signing: sign using either gpg or ssh keys
-: ----------- > 5: 0581c72634c ssh signing: provide a textual representation of the signing key
-: ----------- > 6: 381a950a6e1 ssh signing: parse ssh-keygen output and verify signatures
-: ----------- > 7: 1d292a8d7a2 ssh signing: add test prereqs
1: b8b16f8e6ec ! 8: 338d1b976e9 Add commit, tag & push signing/verification via SSH keys using ssh-keygen
@@ Metadata
Author: Fabian Stelzer <[email protected]>
## Commit message ##
- Add commit, tag & push signing/verification via SSH keys using ssh-keygen
-
- 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.
-
- 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.
-
- Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) "ALLOWED
- SIGNERS") which contains valid public keys and an 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.
-
- 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.
+ ssh signing: duplicate t7510 tests for commits
Signed-off-by: Fabian Stelzer <[email protected]>
- ## Documentation/config/gpg.txt ##
-@@ Documentation/config/gpg.txt: 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
-@@ Documentation/config/gpg.txt: 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.
-
- ## Documentation/config/user.txt ##
-@@ Documentation/config/user.txt: 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.
-
- ## builtin/receive-pack.c ##
-@@ builtin/receive-pack.c: static int receive_pack_config(const char *var, const char *value, void *cb)
- {
- int status = parse_hide_refs_config(var, value, "receive");
-
-+ git_gpg_config(var, value, NULL);
-+
- if (status)
- return status;
-
-@@ builtin/receive-pack.c: static void prepare_push_cert_sha1(struct child_process *proc)
- bogs = parse_signed_buffer(push_cert.buf, push_cert.len);
- check_signature(push_cert.buf, bogs, push_cert.buf + bogs,
- push_cert.len - bogs, &sigcheck);
--
-+
- nonce_status = check_nonce(push_cert.buf, bogs);
- }
- if (!is_null_oid(&push_cert_oid)) {
-
- ## fmt-merge-msg.c ##
-@@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)
- len = payload.len;
- if (check_signature(payload.buf, payload.len, sig.buf,
- sig.len, &sigc) &&
-- !sigc.gpg_output)
-+ !sigc.output)
- strbuf_addstr(&sig, "gpg verification failed.\n");
- else
-- strbuf_addstr(&sig, sigc.gpg_output);
-+ strbuf_addstr(&sig, sigc.output);
- }
- signature_check_clear(&sigc);
-
-
- ## gpg-interface.c ##
-@@
- #include "config.h"
- #include "run-command.h"
- #include "strbuf.h"
-+#include "dir.h"
- #include "gpg-interface.h"
- #include "sigchain.h"
- #include "tempfile.h"
-
- static char *configured_signing_key;
-+const char *ssh_allowed_signers, *ssh_revocation_file;
- static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
-
- struct gpg_format {
-@@ gpg-interface.c: static const char *x509_sigs[] = {
- NULL
- };
-
-+static const char *ssh_verify_args[] = {
-+ NULL
-+};
-+static const char *ssh_sigs[] = {
-+ "-----BEGIN SSH SIGNATURE-----",
-+ NULL
-+};
-+
- static struct gpg_format gpg_format[] = {
- { .name = "openpgp", .program = "gpg",
- .verify_args = openpgp_verify_args,
-@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
- .verify_args = x509_verify_args,
- .sigs = x509_sigs
- },
-+ { .name = "ssh", .program = "ssh-keygen",
-+ .verify_args = ssh_verify_args,
-+ .sigs = ssh_sigs },
- };
-
- static struct gpg_format *use_format = &gpg_format[0];
-@@ gpg-interface.c: static struct gpg_format *get_format_by_sig(const char *sig)
- void signature_check_clear(struct signature_check *sigc)
- {
- FREE_AND_NULL(sigc->payload);
-+ FREE_AND_NULL(sigc->output);
- FREE_AND_NULL(sigc->gpg_output);
- FREE_AND_NULL(sigc->gpg_status);
- FREE_AND_NULL(sigc->signer);
-@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,
- return 1;
- }
-
-+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
-+ */
-+
-+ output = xmemdupz(sigc->output, strcspn(sigc->output, " \n"));
-+ if (skip_prefix(sigc->output, "Good \"git\" signature for ", &output)) {
-+ // Valid signature for a trusted signer
-+ 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;
-+ next = strchrnul(output, '\n'); // key
-+ replace_cstring(&sigc->fingerprint, output, next);
-+ replace_cstring(&sigc->key, output, next);
-+ } else if (skip_prefix(sigc->output, "Good \"git\" signature with ", &output)) {
-+ // Valid signature, but key unknown
-+ sigc->result = 'G';
-+ sigc->trust_level = TRUST_UNDEFINED;
-+
-+ next = strchrnul(output, ' '); // KEY Type
-+ output = next + 1;
-+ next = strchrnul(output, ' '); // 'key'
-+ output = next + 1;
-+ next = strchrnul(output, '\n'); // key
-+ replace_cstring(&sigc->fingerprint, output, next);
-+ replace_cstring(&sigc->key, output, next);
-+ } else {
-+ sigc->result = 'B';
-+ sigc->trust_level = TRUST_NEVER;
-+ }
-+}
-+
- static void parse_gpg_output(struct signature_check *sigc)
- {
- const char *buf = sigc->gpg_status;
-@@ gpg-interface.c: error:
- FREE_AND_NULL(sigc->key);
- }
-
--static int verify_signed_buffer(const char *payload, size_t payload_size,
-- const char *signature, size_t signature_size,
-- struct strbuf *gpg_output,
-- struct strbuf *gpg_status)
-+static int verify_ssh_signature(struct signature_check *sigc, struct gpg_format *fmt,
-+ const char *payload, size_t payload_size,
-+ const char *signature, size_t signature_size)
- {
-- struct child_process gpg = CHILD_PROCESS_INIT;
-- struct gpg_format *fmt;
-+ struct child_process ssh_keygen = CHILD_PROCESS_INIT;
- struct tempfile *temp;
- int ret;
-- struct strbuf buf = STRBUF_INIT;
-+ const char *line;
-+ size_t trust_size;
-+ char *principal;
-+ struct strbuf ssh_keygen_out = STRBUF_INIT;
-+ struct strbuf ssh_keygen_err = STRBUF_INIT;
-
- temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
- if (!temp)
-@@ gpg-interface.c: 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);
-+
-+ return ret;
-+}
-+
-+static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,
-+ const char *payload, size_t payload_size,
-+ const char *signature, size_t signature_size)
-+{
-+ struct child_process gpg = CHILD_PROCESS_INIT;
-+ struct tempfile *temp;
-+ int ret;
-+ struct strbuf gpg_out = STRBUF_INIT;
-+ struct strbuf gpg_err = STRBUF_INIT;
-+
-+ temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
-+ if (!temp)
-+ return error_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);
-
- return ret;
- }
-@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,
- int check_signature(const char *payload, size_t plen, const char *signature,
- size_t slen, struct signature_check *sigc)
- {
-- struct strbuf gpg_output = STRBUF_INIT;
-- struct strbuf gpg_status = STRBUF_INIT;
-+ struct gpg_format *fmt;
- int status;
-
- sigc->result = 'N';
- sigc->trust_level = -1;
-
-- status = verify_signed_buffer(payload, plen, signature, slen,
-- &gpg_output, &gpg_status);
-- if (status && !gpg_output.len)
-- goto out;
-- sigc->payload = xmemdupz(payload, plen);
-- sigc->gpg_output = strbuf_detach(&gpg_output, NULL);
-- sigc->gpg_status = strbuf_detach(&gpg_status, NULL);
-- parse_gpg_output(sigc);
-+ fmt = get_format_by_sig(signature);
-+ if (!fmt)
-+ BUG("bad signature '%s'", signature);
-+
-+ if (!strcmp(fmt->name, "ssh")) {
-+ status = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);
-+ } else {
-+ status = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);
-+ }
-+ if (status && !sigc->gpg_output)
-+ return !!status;
-+
- status |= sigc->result != 'G';
- status |= sigc->trust_level < configured_min_trust_level;
-
-- out:
-- strbuf_release(&gpg_status);
-- strbuf_release(&gpg_output);
--
- return !!status;
- }
-
- void print_signature_buffer(const struct signature_check *sigc, unsigned flags)
- {
- const char *output = flags & GPG_VERIFY_RAW ?
-- sigc->gpg_status : sigc->gpg_output;
-+ sigc->gpg_status : sigc->output;
-
- if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
- fputs(sigc->payload, stdout);
-@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
- int ret;
-
- if (!strcmp(var, "user.signingkey")) {
-+ /* user.signingkey can contain one of the following
-+ * when format = openpgp/x509
-+ * - GPG KeyID
-+ * when format = ssh
-+ * - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)
-+ * - path to a file containing a public or a private ssh key
-+ */
- if (!value)
- return config_error_nonbool(var);
- set_signing_key(value);
- return 0;
- }
-
-+ if (!strcmp(var, "gpg.ssh.keyring")) {
-+ if (!value)
-+ return config_error_nonbool(var);
-+ return git_config_string(&ssh_allowed_signers, var, value);
-+ }
-+
-+ if (!strcmp(var, "gpg.ssh.revocationkeyring")) {
-+ if (!value)
-+ return config_error_nonbool(var);
-+ return git_config_string(&ssh_revocation_file, var, value);
-+ }
-+
- if (!strcmp(var, "gpg.format")) {
- if (!value)
- return config_error_nonbool(var);
-@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
- if (!strcmp(var, "gpg.x509.program"))
- fmtname = "x509";
-
-+ if (!strcmp(var, "gpg.ssh.program"))
-+ fmtname = "ssh";
-+
- if (fmtname) {
- fmt = get_format_by_name(fmtname);
- return git_config_string(&fmt->program, var, value);
-@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
- return 0;
- }
-
-+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);
-+}
-+
-+// 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);
-+ }
-+
-+ return "";
-+}
-+
-+// Returns a textual but unique representation ot the signing key
-+const char *get_signing_key_id(void) {
-+ if (!strcmp(use_format->name, "ssh")) {
-+ return get_ssh_key_fingerprint(get_signing_key());
-+ } else {
-+ // GPG/GPGSM only store a key id on this variable
-+ return get_signing_key();
-+ }
-+}
-+
- const char *get_signing_key(void)
- {
- if (configured_signing_key)
- return configured_signing_key;
-- return git_committer_info(IDENT_STRICT|IDENT_NO_DATE);
-+ if (!strcmp(use_format->name, "ssh")) {
-+ return get_default_ssh_signing_key();
-+ } else {
-+ return git_committer_info(IDENT_STRICT | IDENT_NO_DATE);
-+ }
-+}
-+
-+const char *get_ssh_allowed_signers(void)
-+{
-+ if (ssh_allowed_signers)
-+ return ssh_allowed_signers;
-+
-+ die("A Path to an allowed signers ssh keyring is needed for validation");
- }
-
- int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
-@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
- int ret;
- size_t i, j, bottom;
- struct strbuf gpg_status = STRBUF_INIT;
-+ struct tempfile *temp = NULL, *buffer_file = NULL;
-+ char *ssh_signing_key_file = NULL;
-+ struct strbuf ssh_signature_filename = STRBUF_INIT;
-+
-+ if (!strcmp(use_format->name, "ssh")) {
-+ if (!signing_key || signing_key[0] == '\0')
-+ return error(_("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)
-+ return error_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)
-+ return error_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);
-+
-+ /*
-+ * When the username signingkey is bad, program could be terminated
-+ * because gpg exits without reading and then write gets SIGPIPE.
-+ */
-+ sigchain_push(SIGPIPE, SIG_IGN);
-+ ret = pipe_command(&gpg, buffer->buf, buffer->len, signature, 1024, &gpg_status, 0);
-+ sigchain_pop(SIGPIPE);
-+ }
-
- bottom = signature->len;
-
-- /*
-- * When the username signingkey is bad, program could be terminated
-- * because gpg exits without reading and then write gets SIGPIPE.
-- */
-- 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)
- return error(_("gpg failed to sign the data"));
-
- ## gpg-interface.h ##
-@@ gpg-interface.h: enum signature_trust_level {
-
- struct signature_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":
-@@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature,
- int git_gpg_config(const char *, const char *, void *);
- void set_signing_key(const char *);
- const char *get_signing_key(void);
-+
-+/* Returns a textual unique representation of the signing key in use
-+ * Either a GPG KeyID or a SSH Key Fingerprint
-+ */
-+const char *get_signing_key_id(void);
-+
-+const char *get_ssh_allowed_signers(void);
- int check_signature(const char *payload, size_t plen,
- const char *signature, size_t slen,
- struct signature_check *sigc);
-
- ## log-tree.c ##
-@@ log-tree.c: static void show_signature(struct rev_info *opt, struct commit *commit)
-
- status = check_signature(payload.buf, payload.len, signature.buf,
- signature.len, &sigc);
-- if (status && !sigc.gpg_output)
-+ if (status && !sigc.output)
- show_sig_lines(opt, status, "No signature\n");
- else
-- show_sig_lines(opt, status, sigc.gpg_output);
-+ show_sig_lines(opt, status, sigc.output);
- signature_check_clear(&sigc);
-
- out:
-@@ log-tree.c: 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);
- else
- strbuf_addstr(&verify_message, "No signature\n");
- signature_check_clear(&sigc);
-
- ## pretty.c ##
-@@ pretty.c: static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
- check_commit_signature(c->commit, &(c->signature_check));
- switch (placeholder[1]) {
- case 'G':
-- if (c->signature_check.gpg_output)
-- strbuf_addstr(sb, c->signature_check.gpg_output);
-+ if (c->signature_check.output)
-+ strbuf_addstr(sb, c->signature_check.output);
- break;
- case '?':
- switch (c->signature_check.result) {
-
- ## send-pack.c ##
-@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,
- const struct ref *ref;
- struct string_list_item *item;
- char *signing_key = xstrdup(get_signing_key());
-+ char *signing_key_id = xstrdup(get_signing_key_id());
- const char *cp, *np;
- struct strbuf cert = STRBUF_INIT;
- int update_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) {
-@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,
-
- free_return:
- free(signing_key);
-+ free(signing_key_id);
- strbuf_release(&cert);
- return update_seen;
- }
-
- ## t/lib-gpg.sh ##
-@@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
- echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null
- '
-
-+test_lazy_prereq GPGSSH '
-+ ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1)
-+ test $? != 127 || exit 1
-+ echo $ssh_version | grep -q "find-principals:missing signature file"
-+ 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_\" NR \" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.keyring" &&
-+ cat "${GNUPGHOME}/ssh.all_valid.keyring" &&
-+ ssh-keygen -t ed25519 -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_KEYRING="${GNUPGHOME}/ssh.all_valid.keyring"
-+
-+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"
-+
- sanitize_pgp() {
- perl -ne '
- /^-----END PGP/ and $in_pgp = 0;
-
- ## t/t4202-log.sh ##
-@@ t/t4202-log.sh: test_expect_success GPGSM 'setup signed branch x509' '
- git commit -S -m signed_commit
- '
-
-+test_expect_success GPGSSH 'setup sshkey signed branch' '
-+ test_config gpg.format ssh &&
-+ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
-+ test_when_finished "git reset --hard && git checkout main" &&
-+ git checkout -b signed-ssh main &&
-+ echo foo >foo &&
-+ git add foo &&
-+ git commit -S -m signed_commit
-+'
-+
- test_expect_success GPGSM 'log x509 fingerprint' '
- echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect &&
- git log -n1 --format="%GF | %GP" signed-x509 >actual &&
-@@ t/t4202-log.sh: test_expect_success GPGSM 'log --graph --show-signature x509' '
- grep "^| gpgsm: Good signature" actual
- '
-
-+test_expect_success GPGSSH 'log ssh key fingerprint' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+ ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect &&
-+ git log -n1 --format="%GF | %GP" signed-ssh >actual &&
-+ test_cmp expect actual
-+'
-+
- test_expect_success GPG 'log --graph --show-signature for merged tag' '
- test_when_finished "git reset --hard && git checkout main" &&
- git checkout -b plain main &&
-
- ## t/t5534-push-signed.sh ##
-@@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certificate' '
- test_cmp expect dst/push-cert-status
- '
-
-+test_expect_success GPGSSH 'ssh signed push sends push certificate' '
-+ prepare_dst &&
-+ mkdir -p dst/.git/hooks &&
-+ git -C dst config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+ git -C dst config receive.certnonceseed sekrit &&
-+ write_script dst/.git/hooks/post-receive <<-\EOF &&
-+ # discard the update list
-+ cat >/dev/null
-+ # record the push certificate
-+ if test -n "${GIT_PUSH_CERT-}"
-+ then
-+ git cat-file blob $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_config gpg.format ssh &&
-+ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
-+ FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") &&
-+ git push --signed dst noop ff +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=$(git rev-parse noop) &&
-+ ff=$(git rev-parse ff) &&
-+ noff=$(git rev-parse noff) &&
-+ grep "$noop $ff refs/heads/ff" dst/push-cert &&
-+ grep "$noop $noff refs/heads/noff" dst/push-cert &&
-+ test_cmp expect dst/push-cert-status
-+'
-+
- test_expect_success GPG 'inconsistent push options in signed push not allowed' '
- # First, invoke receive-pack with dummy input to obtain its preamble.
- prepare_dst &&
-@@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed user.signingkey x509' '
- test_cmp expect dst/push-cert-status
- '
-
-+test_expect_success GPGSSH 'fail without key and heed user.signingkey ssh' '
-+ test_config gpg.format ssh &&
-+ prepare_dst &&
-+ mkdir -p dst/.git/hooks &&
-+ git -C dst config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+ git -C dst config receive.certnonceseed sekrit &&
-+ write_script dst/.git/hooks/post-receive <<-\EOF &&
-+ # discard the update list
-+ cat >/dev/null
-+ # record the push certificate
-+ if test -n "${GIT_PUSH_CERT-}"
-+ then
-+ git cat-file blob $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_config user.email [email protected] &&
-+ test_config gpg.format ssh &&
-+
-+ test_config user.signingkey "" &&
-+ (
-+ sane_unset GIT_COMMITTER_EMAIL &&
-+ test_must_fail git push --signed dst noop ff +noff
-+ ) &&
-+ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
-+ FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") &&
-+ git push --signed dst noop ff +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=$(git rev-parse noop) &&
-+ ff=$(git rev-parse ff) &&
-+ noff=$(git rev-parse noff) &&
-+ grep "$noop $ff refs/heads/ff" dst/push-cert &&
-+ grep "$noop $noff refs/heads/noff" dst/push-cert &&
-+ test_cmp expect dst/push-cert-status
-+'
-+
- test_expect_success GPG 'failed atomic push does not execute GPG' '
- prepare_dst &&
- git -C dst config receive.certnonceseed sekrit &&
-
- ## t/t7031-verify-tag-signed-ssh.sh (new) ##
-@@
-+#!/bin/sh
-+
-+test_description='signed tag tests'
-+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
-+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
-+
-+. ./test-lib.sh
-+. "$TEST_DIRECTORY/lib-gpg.sh"
-+
-+test_expect_success GPGSSH 'create signed tags ssh' '
-+ test_when_finished "test_unconfig commit.gpgsign" &&
-+ test_config gpg.format ssh &&
-+ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
-+
-+ echo 1 >file && git add file &&
-+ test_tick && git commit -m initial &&
-+ git tag -s -m initial initial &&
-+ git branch side &&
-+
-+ echo 2 >file && test_tick && git commit -a -m second &&
-+ git tag -s -m second second &&
-+
-+ git checkout side &&
-+ echo 3 >elif && git add elif &&
-+ test_tick && git commit -m "third on side" &&
-+
-+ git checkout main &&
-+ test_tick && git merge -S side &&
-+ git tag -s -m merge merge &&
-+
-+ echo 4 >file && test_tick && git commit -a -S -m "fourth unsigned" &&
-+ git tag -a -m fourth-unsigned fourth-unsigned &&
-+
-+ test_tick && git commit --amend -S -m "fourth signed" &&
-+ git tag -s -m fourth fourth-signed &&
-+
-+ echo 5 >file && test_tick && git commit -a -m "fifth" &&
-+ git tag fifth-unsigned &&
-+
-+ git config commit.gpgsign true &&
-+ echo 6 >file && test_tick && git commit -a -m "sixth" &&
-+ git tag -a -m sixth sixth-unsigned &&
-+
-+ test_tick && git rebase -f HEAD^^ && git tag -s -m 6th sixth-signed HEAD^ &&
-+ git tag -m seventh -s seventh-signed &&
-+
-+ echo 8 >file && test_tick && git commit -a -m eighth &&
-+ git tag -u"${SIGNING_KEY_UNTRUSTED}" -m eighth eighth-signed-alt
-+'
-+
-+test_expect_success GPGSSH 'verify and show ssh signatures' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+ test_config gpg.mintrustlevel UNDEFINED &&
-+ (
-+ for tag in initial second merge fourth-signed sixth-signed seventh-signed
-+ do
-+ git verify-tag $tag 2>actual &&
-+ grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
-+ ! grep "${BAD_SIGNATURE}" actual &&
-+ echo $tag OK || exit 1
-+ done
-+ ) &&
-+ (
-+ for tag in fourth-unsigned fifth-unsigned sixth-unsigned
-+ do
-+ test_must_fail git verify-tag $tag 2>actual &&
-+ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
-+ ! grep "${BAD_SIGNATURE}" actual &&
-+ echo $tag OK || exit 1
-+ done
-+ ) &&
-+ (
-+ for tag in eighth-signed-alt
-+ do
-+ git verify-tag $tag 2>actual &&
-+ grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
-+ ! grep "${BAD_SIGNATURE}" actual &&
-+ grep "${KEY_NOT_TRUSTED}" actual &&
-+ echo $tag OK || exit 1
-+ done
-+ )
-+'
-+
-+test_expect_success GPGSSH 'detect fudged ssh signature' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+ git cat-file tag seventh-signed >raw &&
-+ sed -e "/^tag / s/seventh/7th forged/" raw >forged1 &&
-+ git hash-object -w -t tag forged1 >forged1.tag &&
-+ test_must_fail git verify-tag $(cat forged1.tag) 2>actual1 &&
-+ grep "${BAD_SIGNATURE}" actual1 &&
-+ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual1 &&
-+ ! grep "${GOOD_SIGNATURE_UNTRUSTED}" actual1
-+'
-+
-+# test_expect_success GPGSSH 'verify ssh signatures with --raw' '
-+# test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+# (
-+# for tag in initial second merge fourth-signed sixth-signed seventh-signed
-+# do
-+# git verify-tag --raw $tag 2>actual &&
-+# grep "GOODSIG" actual &&
-+# ! grep "BADSIG" actual &&
-+# echo $tag OK || exit 1
-+# done
-+# ) &&
-+# (
-+# for tag in fourth-unsigned fifth-unsigned sixth-unsigned
-+# do
-+# test_must_fail git verify-tag --raw $tag 2>actual &&
-+# ! grep "GOODSIG" actual &&
-+# ! grep "BADSIG" actual &&
-+# echo $tag OK || exit 1
-+# done
-+# ) &&
-+# (
-+# for tag in eighth-signed-alt
-+# do
-+# git verify-tag --raw $tag 2>actual &&
-+# grep "GOODSIG" actual &&
-+# ! grep "BADSIG" actual &&
-+# grep "TRUST_UNDEFINED" actual &&
-+# echo $tag OK || exit 1
-+# done
-+# )
-+# '
-+
-+# test_expect_success GPGSM 'verify signatures with --raw x509' '
-+# git verify-tag --raw ninth-signed-x509 2>actual &&
-+# grep "GOODSIG" actual &&
-+# ! grep "BADSIG" actual &&
-+# echo ninth-signed-x509 OK
-+# '
-+
-+# test_expect_success GPGSSH 'verify multiple tags' '
-+# test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+# tags="fourth-signed sixth-signed seventh-signed" &&
-+# for i in $tags
-+# do
-+# git verify-tag -v --raw $i || return 1
-+# done >expect.stdout 2>expect.stderr.1 &&
-+# grep "^.GNUPG:." <expect.stderr.1 >expect.stderr &&
-+# git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
-+# grep "^.GNUPG:." <actual.stderr.1 >actual.stderr &&
-+# test_cmp expect.stdout actual.stdout &&
-+# test_cmp expect.stderr actual.stderr
-+# '
-+
-+# test_expect_success GPGSM 'verify multiple tags x509' '
-+# test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+# tags="seventh-signed ninth-signed-x509" &&
-+# for i in $tags
-+# do
-+# git verify-tag -v --raw $i || return 1
-+# done >expect.stdout 2>expect.stderr.1 &&
-+# grep "^.GNUPG:." <expect.stderr.1 >expect.stderr &&
-+# git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
-+# grep "^.GNUPG:." <actual.stderr.1 >actual.stderr &&
-+# test_cmp expect.stdout actual.stdout &&
-+# test_cmp expect.stderr actual.stderr
-+# '
-+
-+test_expect_success GPGSSH 'verifying tag with --format' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
-+ cat >expect <<-\EOF &&
-+ tagname : fourth-signed
-+ EOF
-+ git verify-tag --format="tagname : %(tag)" "fourth-signed" >actual &&
-+ test_cmp expect actual
-+'
-+
-+test_expect_success GPGSSH 'verifying a forged tag with --format should fail silently' '
-+ test_must_fail git verify-tag --format="tagname : %(tag)" $(cat forged1.tag) >actual-forged &&
-+ test_must_be_empty actual-forged
-+'
-+
-+test_done
-
## t/t7527-signed-commit-ssh.sh (new) ##
@@
+#!/bin/sh
@@ t/t7527-signed-commit-ssh.sh (new)
+ test_must_fail git verify-commit eighth-signed-alt
+'
+
-+# test_expect_success GPGSSH 'verify signatures with --raw' '
-+# (
-+# for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
-+# do
-+# git verify-commit --raw $commit 2>actual &&
-+# grep "GOODSIG" actual &&
-+# ! grep "BADSIG" actual &&
-+# echo $commit OK || exit 1
-+# done
-+# ) &&
-+# (
-+# for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned
-+# do
-+# test_must_fail git verify-commit --raw $commit 2>actual &&
-+# ! grep "GOODSIG" actual &&
-+# ! grep "BADSIG" actual &&
-+# echo $commit OK || exit 1
-+# done
-+# ) &&
-+# (
-+# for commit in eighth-signed-alt
-+# do
-+# git verify-commit --raw $commit 2>actual &&
-+# grep "GOODSIG" actual &&
-+# ! grep "BADSIG" actual &&
-+# grep "TRUST_UNDEFINED" actual &&
-+# echo $commit OK || exit 1
-+# done
-+# )
-+# '
++test_expect_success GPGSSH 'verify signatures with --raw' '
++ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ (
++ for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
++ do
++ git verify-commit --raw $commit 2>actual &&
++ grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ echo $commit OK || exit 1
++ done
++ ) &&
++ (
++ for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned
++ do
++ test_must_fail git verify-commit --raw $commit 2>actual &&
++ ! grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ echo $commit OK || exit 1
++ done
++ ) &&
++ (
++ for commit in eighth-signed-alt
++ do
++ git verify-commit --raw $commit 2>actual &&
++ grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
++ ! grep "${BAD_SIGNATURE}" actual &&
++ echo $commit OK || exit 1
++ done
++ )
++'
+
+test_expect_success GPGSSH 'proper header is used for hash algorithm' '
+ git cat-file commit fourth-signed >output &&
@@ t/t7527-signed-commit-ssh.sh (new)
+ FINGERPRINT
+ principal_1
+ FINGERPRINT
-+
++
+ EOF
+ sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect &&
+ git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual &&
@@ t/t7527-signed-commit-ssh.sh (new)
+ FINGERPRINT
+
+ FINGERPRINT
-+
++
+ EOF
+ git log -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;}") &&
@@ t/t7527-signed-commit-ssh.sh (new)
+ test_must_fail git commit -S --amend -m "fail"
+'
+
-+# test_expect_success GPGSSH 'detect fudged commit with double signature' '
-+# sed -e "/gpgsig/,/END PGP/d" forged1 >double-base &&
-+# sed -n -e "/gpgsig/,/END PGP/p" forged1 | \
-+# sed -e "s/^$(test_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig &&
-+# gpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&
-+# cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&
-+# sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \
-+# double-combined.asc > double-gpgsig &&
-+# sed -e "/committer/r double-gpgsig" double-base >double-commit &&
-+# git hash-object -w -t commit double-commit >double-commit.commit &&
-+# test_must_fail git verify-commit $(cat double-commit.commit) &&
-+# git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&
-+# grep "BAD signature from" double-actual &&
-+# grep "Good signature from" double-actual
-+# '
++test_expect_failure GPGSSH '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_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig &&
++ gpg -o double-sig2.sig -u 29472784 --detach-sign double-base &&
++ cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc &&
++ sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \
++ double-combined.asc > double-gpgsig &&
++ sed -e "/committer/r double-gpgsig" double-base >double-commit &&
++ git hash-object -w -t commit double-commit >double-commit.commit &&
++ test_must_fail git verify-commit $(cat double-commit.commit) &&
++ git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual &&
++ grep "BAD signature from" double-actual &&
++ grep "Good signature from" double-actual
++'
+
-+# test_expect_success GPGSSH 'show double signature with custom format' '
-+# cat >expect <<-\EOF &&
-+# E
++test_expect_failure GPGSSH 'show double signature with custom format (TODO)' '
++ cat >expect <<-\EOF &&
++ E
+
+
+
+
-+# EOF
-+# git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual &&
-+# test_cmp expect actual
-+# '
++ EOF
++ git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual &&
++ test_cmp expect actual
++'
+
+
-+# test_expect_success GPGSSH 'verify-commit verifies multiply signed commits' '
-+# git init multiply-signed &&
-+# cd multiply-signed &&
-+# test_commit first &&
-+# echo 1 >second &&
-+# git add second &&
-+# tree=$(git write-tree) &&
-+# parent=$(git rev-parse HEAD^{commit}) &&
-+# git commit --gpg-sign -m second &&
-+# git cat-file commit HEAD &&
-+# # Avoid trailing whitespace.
-+# sed -e "s/^Q//" -e "s/^Z/ /" >commit <<-EOF &&
-+# Qtree $tree
-+# Qparent $parent
-+# Qauthor A U Thor <[email protected]> 1112912653 -0700
-+# Qcommitter C O Mitter <[email protected]> 1112912653 -0700
-+# Qgpgsig -----BEGIN PGP SIGNATURE-----
-+# QZ
-+# Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy
-+# Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC
-+# Q AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==
-+# Q =tQ0N
-+# Q -----END PGP SIGNATURE-----
-+# Qgpgsig-sha256 -----BEGIN PGP SIGNATURE-----
-+# QZ
-+# Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy
-+# Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO
-+# Q AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==
-+# Q =pIwP
-+# Q -----END PGP SIGNATURE-----
-+# Q
-+# Qsecond
-+# EOF
-+# head=$(git hash-object -t commit -w commit) &&
-+# git reset --hard $head &&
-+# git verify-commit $head 2>actual &&
-+# grep "Good signature from" actual &&
-+# ! grep "BAD signature from" actual
-+# '
++test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' '
++ git init multiply-signed &&
++ cd multiply-signed &&
++ test_commit first &&
++ echo 1 >second &&
++ git add second &&
++ tree=$(git write-tree) &&
++ parent=$(git rev-parse HEAD^{commit}) &&
++ git commit --gpg-sign -m second &&
++ git cat-file commit HEAD &&
++ # Avoid trailing whitespace.
++ sed -e "s/^Q//" -e "s/^Z/ /" >commit <<-EOF &&
++ Qtree $tree
++ Qparent $parent
++ Qauthor A U Thor <[email protected]> 1112912653 -0700
++ Qcommitter C O Mitter <[email protected]> 1112912653 -0700
++ Qgpgsig -----BEGIN PGP SIGNATURE-----
++ QZ
++ Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy
++ Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC
++ Q AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==
++ Q =tQ0N
++ Q -----END PGP SIGNATURE-----
++ Qgpgsig-sha256 -----BEGIN PGP SIGNATURE-----
++ QZ
++ Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy
++ Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO
++ Q AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==
++ Q =pIwP
++ Q -----END PGP SIGNATURE-----
++ Q
++ Qsecond
++ EOF
++ head=$(git hash-object -t commit -w commit) &&
++ git reset --hard $head &&
++ git verify-commit $head 2>actual &&
++ grep "Good signature from" actual &&
++ ! grep "BAD signature from" actual
++'
+
+test_done -: ----------- > 9: 33330fda441 ssh signing: add more tests for logs, tags & push certs
Signature · 1 line
gitgitgadget
PATCH v3 3/9 ssh signing: retrieve a default key from ssh-agent
From: Fabian Stelzer <[email protected]>
calls ssh-add -L and uses the first key
--- gpg-interface.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+)
gpg-interface.c +17 -0
@@ -467,6 +467,23 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +/* 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 ); + } + +return "" ; +}const char *get_signing_key (void ) {if (configured_signing_key)
Signature · 1 line
gitgitgadget
PATCH v3 4/9 ssh signing: sign using either gpg or ssh keys
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- gpg-interface.c | 86 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 10 deletions(-)
gpg-interface.c +76 -10
@@ -505,30 +505,96 @@ const char *get_ssh_allowed_signers(void)int sign_buffer (struct strbuf *buffer,struct strbuf *signature,const char *signing_key) { -struct child_process gpg =CHILD_PROCESS_INIT ; +struct child_process signer =CHILD_PROCESS_INIT ;int ret;size_t i, j, bottom; -struct strbuf gpg_status =STRBUF_INIT ; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *temp =NULL , *buffer_file =NULL ; +char *ssh_signing_key_file =NULL ; +struct strbuf ssh_signature_filename =STRBUF_INIT ; -strvec_pushl (&gpg.args, - use_format->program, +if (!strcmp (use_format->name,"ssh" )) { +if (!signing_key || signing_key[0 ] =='\0' ) +return error (_ ("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) +return error_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) +return error_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; -/* * When the username signingkey is bad, program could be terminated * because gpg exits without reading and then write gets SIGPIPE. */ 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)return error (_ ("gpg failed to sign the data" ));
Signature · 1 line
gitgitgadget
PATCH v3 5/9 ssh signing: provide a textual representation of the signing key
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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
--- gpg-interface.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ gpg-interface.h | 7 +++++++ send-pack.c | 8 ++++---- 3 files changed, 58 insertions(+), 4 deletions(-)
gpg-interface.c +47 -0
@@ -467,6 +467,42 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +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; + +/* +* With 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 ]) +die_errno (_ ("failed to get the ssh fingerprint for key '%s'" ), + signing_key); + +return strbuf_detach (fingerprint[1 ],NULL ); +} +/* 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 ;
@@ -484,6 +520,17 @@ static char *get_default_ssh_signing_key(void) {return "" ; } + +/* Returns a textual but unique representation ot the signing key */ +const char *get_signing_key_id (void ) { +if (!strcmp (use_format->name,"ssh" )) { +return get_ssh_key_fingerprint (get_signing_key ()); + }else { +/* GPG/GPGSM only store a key id on this variable */ +return get_signing_key (); + } +} +const char *get_signing_key (void ) {if (configured_signing_key)
gpg-interface.h +7 -0
@@ -64,6 +64,13 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); + +/* +* Returns a textual unique representation of the signing key in use +* Either a GPG KeyID or a SSH Key Fingerprint +*/ +const char *get_signing_key_id (void ); +const char *get_ssh_allowed_signers (void ); int check_signature(const char *payload, size_t plen, const char *signature, size_t slen,
send-pack.c +4 -4
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf, {const struct ref *ref;struct string_list_item *item; -char *signing_key =xstrdup (get_signing_key ()); +char *signing_key_id =xstrdup (get_signing_key_id ());const char *cp, *np;struct strbuf cert =STRBUF_INIT ;int update_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) goto free_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);return update_seen; }
Signature · 1 line
gitgitgadget
PATCH v3 6/9 ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- builtin/receive-pack.c | 2 + gpg-interface.c | 139 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+)
builtin/receive-pack.c +2 -0
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) {int status =parse_hide_refs_config (var, value,"receive" ); +git_gpg_config (var, value,NULL ); +if (status)return status;
gpg-interface.c +139 -0
157 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v3 7/9 ssh signing: add test prereqs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
generate some ssh keys and a allowed keys keyring for testing
--- t/lib-gpg.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)
t/lib-gpg.sh +27 -0
@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 ' echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null ' +test_lazy_prereq GPGSSH ' + ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1) + test $? != 127 || exit 1 + echo $ssh_version | grep -q "find-principals:missing signature file" + 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_\" NR \" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.keyring" && + cat "${GNUPGHOME}/ssh.all_valid.keyring" && + ssh-keygen -t ed25519 -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_KEYRING="${GNUPGHOME}/ssh.all_valid.keyring" + +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" + sanitize_pgp() { perl -ne ' /^-----END PGP/ and $in_pgp = 0;
Signature · 1 line
gitgitgadget
PATCH v3 8/9 ssh signing: duplicate t7510 tests for commits
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100755 t/t7527-signed-commit-ssh.sh
t/t7527-signed-commit-ssh.sh new file +398 -0
@@ -0,0 +1,398 @@ +#!/bin/sh + +test_description='ssh signed commit tests' +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh +GNUPGHOME_NOT_USED=$GNUPGHOME +. "$TEST_DIRECTORY/lib-gpg.sh" + +test_expect_success GPGSSH 'create signed commits' ' + test_oid_cache <<-\EOF && + header sha1:gpgsig + header sha256:gpgsig-sha256 + EOF + + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + + echo 1 >file && git add file && + test_tick && git commit -S -m initial && + git tag initial && + git branch side && + + echo 2 >file && test_tick && git commit -a -S -m second && + git tag second && + + git checkout side && + echo 3 >elif && git add elif && + test_tick && git commit -m "third on side" && + + git checkout main && + test_tick && git merge -S side && + git tag merge && + + echo 4 >file && test_tick && git commit -a -m "fourth unsigned" && + git tag fourth-unsigned && + + test_tick && git commit --amend -S -m "fourth signed" && + git tag fourth-signed && + + git config commit.gpgsign true && + echo 5 >file && test_tick && git commit -a -m "fifth signed" && + git tag fifth-signed && + + git config commit.gpgsign false && + echo 6 >file && test_tick && git commit -a -m "sixth" && + git tag sixth-unsigned && + + git config commit.gpgsign true && + echo 7 >file && test_tick && git commit -a -m "seventh" --no-gpg-sign && + git tag seventh-unsigned && + + test_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ && + git tag seventh-signed && + + echo 8 >file && test_tick && git commit -a -m eighth -S"${SIGNING_KEY_UNTRUSTED}" && + git tag eighth-signed-alt && + + # commit.gpgsign is still on but this must not be signed + echo 9 | git commit-tree HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag ninth-unsigned $(cat oid) && + # explicit -S of course must sign. + echo 10 | git commit-tree -S HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag tenth-signed $(cat oid) && + + # --gpg-sign[=<key-id>] must sign. + echo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag eleventh-signed $(cat oid) && + echo 12 | git commit-tree --gpg-sign="${SIGNING_KEY_UNTRUSTED}" HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag twelfth-signed-alt $(cat oid) +' + +test_expect_success GPGSSH 'verify and show signatures' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + test_config gpg.mintrustlevel UNDEFINED && + ( + for commit in initial second merge fourth-signed \ + fifth-signed sixth-signed seventh-signed tenth-signed \ + eleventh-signed + do + git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned \ + seventh-unsigned ninth-unsigned + do + test_must_fail git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt twelfth-signed-alt + do + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + git verify-commit eighth-signed-alt 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual +' + +test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + test_config gpg.minTrustLevel fully && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + test_config gpg.minTrustLevel marginal && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' ' + test_config gpg.minTrustLevel ultimate && + test_must_fail git verify-commit eighth-signed-alt +' + +test_expect_success GPGSSH 'verify signatures with --raw' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + ( + for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned + do + test_must_fail git verify-commit --raw $commit 2>actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'proper header is used for hash algorithm' ' + git cat-file commit fourth-signed >output && + grep "^$(test_oid header) -----BEGIN SSH SIGNATURE-----" output +' + +test_expect_success GPGSSH 'show signed commit with signature' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + git show -s initial >commit && + git show -s --show-signature initial >show && + git verify-commit -v initial >verify.1 2>verify.2 && + git cat-file commit initial >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_cmp show.commit commit && + test_cmp show.gpg verify.2 && + test_cmp cat.commit verify.1 +' + +test_expect_success GPGSSH 'detect fudged signature' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + git cat-file commit seventh-signed >raw && + sed -e "s/^seventh/7th forged/" raw >forged1 && + git hash-object -w -t commit forged1 >forged1.commit && + test_must_fail git verify-commit $(cat forged1.commit) && + git show --pretty=short --show-signature $(cat forged1.commit) >actual1 && + grep "${BAD_SIGNATURE}" actual1 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual1 && + ! grep "${GOOD_SIGNATURE_UNTRUSTED}" actual1 +' + +test_expect_success GPGSSH 'detect fudged signature with NUL' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + git cat-file commit seventh-signed >raw && + cat raw >forged2 && + echo Qwik | tr "Q" "\000" >>forged2 && + git hash-object -w -t commit forged2 >forged2.commit && + test_must_fail git verify-commit $(cat forged2.commit) && + git show --pretty=short --show-signature $(cat forged2.commit) >actual2 && + grep "${BAD_SIGNATURE}" actual2 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual2 +' + +test_expect_success GPGSSH 'amending already signed commit' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + git checkout fourth-signed^0 && + git commit --amend -S --no-edit && + git verify-commit HEAD && + git show -s --show-signature HEAD >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH 'show good signature with custom format' ' + test_config gpg.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 && + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show bad signature with custom format' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + cat >expect <<-\EOF && + B + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat forged1.commit) >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with custom format' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + cat >expect.tmpl <<-\EOF && + U + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with undefined trust level' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + cat >expect.tmpl <<-\EOF && + undefined + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + cat >expect.tmpl <<-\EOF && + fully + FINGERPRINT + principal_1 + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show lack of signature with custom format' ' + cat >expect <<-\EOF && + N + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" seventh-unsigned >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + test_config log.showsignature true && + git show initial >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH 'check config gpg.format values' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.format ssh && + git commit -S --amend -m "success" && + test_config gpg.format OpEnPgP && + test_must_fail git commit -S --amend -m "fail" +' + +test_expect_failure GPGSSH '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_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig && + gpg -o double-sig2.sig -u 29472784 --detach-sign double-base && + cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc && + sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \ + double-combined.asc > double-gpgsig && + sed -e "/committer/r double-gpgsig" double-base >double-commit && + git hash-object -w -t commit double-commit >double-commit.commit && + test_must_fail git verify-commit $(cat double-commit.commit) && + git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual && + grep "BAD signature from" double-actual && + grep "Good signature from" double-actual +' + +test_expect_failure GPGSSH 'show double signature with custom format (TODO)' ' + cat >expect <<-\EOF && + E + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual && + test_cmp expect actual +' + + +test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' ' + git init multiply-signed && + cd multiply-signed && + test_commit first && + echo 1 >second && + git add second && + tree=$(git write-tree) && + parent=$(git rev-parse HEAD^{commit}) && + git commit --gpg-sign -m second && + git cat-file commit HEAD && + # Avoid trailing whitespace. + sed -e "s/^Q//" -e "s/^Z/ /" >commit <<-EOF && + Qtree $tree + Qparent $parent + Qauthor A U Thor <[email protected]> 1112912653 -0700 + Qcommitter C O Mitter <[email protected]> 1112912653 -0700 + Qgpgsig -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC + Q AKCTqBh+VabJceXcGIZuF0Ry+udbBQ== + Q =tQ0N + Q -----END PGP SIGNATURE----- + Qgpgsig-sha256 -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO + Q AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw== + Q =pIwP + Q -----END PGP SIGNATURE----- + Q + Qsecond + EOF + head=$(git hash-object -t commit -w commit) && + git reset --hard $head && + git verify-commit $head 2>actual && + grep "Good signature from" actual && + ! grep "BAD signature from" actual +' + +test_done
Signature · 1 line
gitgitgadget
PATCH v3 9/9 ssh signing: add more tests for logs, tags & push certs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t4202-log.sh | 23 +++++ t/t5534-push-signed.sh | 101 +++++++++++++++++++ t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100755 t/t7031-verify-tag-signed-ssh.sh
t/t4202-log.sh +23 -0
@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' ' git commit -S -m signed_commit ' +test_expect_success GPGSSH 'setup sshkey signed branch' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_when_finished "git reset --hard && git checkout main" && + git checkout -b signed-ssh main && + echo foo >foo && + git add foo && + git commit -S -m signed_commit +' + test_expect_success GPGSM 'log x509 fingerprint' ' echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && git log -n1 --format="%GF | %GP" signed-x509 >actual &&
@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' ' test_cmp expect actual ' +test_expect_success GPGSSH 'log ssh key fingerprint' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect && + git log -n1 --format="%GF | %GP" signed-ssh >actual && + test_cmp expect actual +' + test_expect_success GPG 'log --graph --show-signature' ' git log --graph --show-signature -n1 signed >actual && grep "^| gpg: Signature made" actual &&
@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' ' grep "^| gpgsm: Good signature" actual ' +test_expect_success GPGSSH 'log --graph --show-signature ssh' ' + test_config gpg.ssh.keyring "${SIGNING_KEYRING}" && + git log --graph --show-signature -n1 signed-ssh >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout main" && git checkout -b plain main &&
t/t5534-push-signed.sh +101 -0
113 lines not on this page · raw message
t/t7031-verify-tag-signed-ssh.sh new file +161 -0
161 lines not on this page · raw message
Signature · 1 line
gitgitgadget
Re: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys
In reply to Fabian Stelzer via GitGitGadgetQuoted 2 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
Subject: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys
If you chose "ssh signing:" as the common prefix for the series, use it consistently with this step, too.
Quoted 6 lines
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.
OK.
Quoted 4 lines
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.
Drop this paragraph or at least tone it down. It may hold true only around your immediate circle but it is far from clear and obvious. I'd expect more people push over https:// than ssh://.
We do not really require a new feature to make much more accessible for wide average user---making it just a bit more accessible to folks in your immediate circle is perfectly fine, as long as you are not harming other people ;-)
Quoted 2 lines
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email signing/encryption and ssh keys which i think is quite common
Upcase "I".
Quoted 8 lines
(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.
All of the above promises a wonderful new world, but what is left unclear is with this step alone how much of the new world we already gain. When you ask others to read and understand your code, please give them a bit more hint to guide them what to expect and where you are taking them next.
Quoted 5 lines
fmt-merge-msg.c +0
-0
fmt-merge-msg.c static void fmt_merge_msg_sigs(struct strbuf *out) · line 533
Quoted 9 lines
fmt-merge-msg.c +2
-2@@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out) len = payload.len;if (check_signature (payload.buf, payload.len, sig.buf, sig.len, &sigc) && - !sigc.gpg_output) + !sigc.output)strbuf_addstr (&sig,"gpg verification failed.\n" );else -strbuf_addstr (&sig, sigc.gpg_output); +strbuf_addstr (&sig, sigc.output);
These are "rename some fields for consistency" the proposed log message promised. Makes sense, as you are taking the sigc structure away from pgp/gpg dependency.
gpg-interface.c line 11
Quoted 9 lines
gpg-interface.c +1
-0@@ -8,6 +8,7 @@#include "tempfile.h" static char *configured_signing_key; +const char *ssh_allowed_signers, *ssh_revocation_file;
Very likely these want to be file-scope statics?
gpg-interface.c static const char *x509_sigs[] = { · line 41
Quoted 10 lines
gpg-interface.c +3
-0static enum signature_trust_level configured_min_trust_level =TRUST_UNDEFINED ;struct gpg_format {@@ -35,6 +36,14 @@ static const char *x509_sigs[] = { NULL }; +static const char *ssh_verify_args[] = { +NULL +};
A blank line is missing from here.
Quoted 29 lines
gpg-interface.c +9
-1+static const char *ssh_sigs[] = { +"-----BEGIN SSH SIGNATURE-----" , +NULL +}; +static struct gpg_format gpg_format[] = { { .name ="openpgp" , .program ="gpg" , .verify_args = openpgp_verify_args,@@ -44,6 +53,9 @@ static struct gpg_format gpg_format[] = { .verify_args = x509_verify_args, .sigs = x509_sigs }, + { .name = "ssh", .program = "ssh-keygen", + .verify_args = ssh_verify_args, + .sigs = ssh_sigs }, };static struct gpg_format *use_format = &gpg_format[0 ];@@ -72,7 +84,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)void signature_check_clear (struct signature_check *sigc) {FREE_AND_NULL (sigc->payload); -FREE_AND_NULL (sigc->gpg_output); +FREE_AND_NULL (sigc->output);FREE_AND_NULL (sigc->gpg_status);FREE_AND_NULL (sigc->signer);FREE_AND_NULL (sigc->key);@@ -257,16 +269,15 @@ error:FREE_AND_NULL (sigc->key);
gpg-interface.c error: · line 275
Quoted 10 lines
gpg-interface.c +3
-4} -static int verify_signed_buffer (const char *payload,size_t payload_size, -const char *signature,size_t signature_size, -struct strbuf *gpg_output, -struct strbuf *gpg_status) +static int verify_gpg_signature (struct signature_check *sigc,struct gpg_format *fmt, +const char *payload,size_t payload_size, +const char *signature,size_t signature_size) {
What is this hunk about? The more generic name "verify-signed-buffer" is rescinded and gets replaced by a more GPG/PGP specific helper?
You'd need to help readers a bit more by explaining in the proposed log message that you shifted the boundary of responsibility between check_signature() and verify_signed_buffer()---it used to be that the latter inspected the signed payload to see if it a valid GPG/PGP signature before doing GPG specific validation, but you want to make the former responsible for calling get_format_by_sig(), so that you can dispatch a totally new backend that sits next to this GPG specific one.
Quoted 20 lines
gpg-interface.c +2
-6struct child_process gpg =CHILD_PROCESS_INIT ; -struct gpg_format *fmt;struct tempfile *temp;int ret; -struct strbuf buf =STRBUF_INIT ; +struct strbuf gpg_out =STRBUF_INIT ; +struct strbuf gpg_err =STRBUF_INIT ; temp =mks_tempfile_t (".git_vtag_tmpXXXXXX" );if (!temp)@@ -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);
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 298
Quoted 10 lines
gpg-interface.c +3
-6strvec_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 );
What is going on around here? Ahh, an unnecessary indentation change is fooling the diff and made the patch unreadable. Sigh...
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 302
Quoted 5 lines
gpg-interface.c +2
-2sigchain_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 );
What is this change about? Is it another unnecessary indentation change? Please make sure you keep distraction to your readers to the minimum.
Quoted 21 lines
gpg-interface.c +2
-10@@ -309,35 +319,36 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc) { -struct strbuf gpg_output =STRBUF_INIT ; -struct strbuf gpg_status =STRBUF_INIT ; +struct gpg_format *fmt;int status; sigc->result ='N' ; sigc->trust_level =-1 ; - status =verify_signed_buffer (payload, plen, signature, slen, - &gpg_output, &gpg_status); -if (status && !gpg_output.len) - goto out; - sigc->payload =xmemdupz (payload, plen); - sigc->gpg_output =strbuf_detach (&gpg_output,NULL ); - sigc->gpg_status =strbuf_detach (&gpg_status,NULL ); -parse_gpg_output (sigc); + fmt =get_format_by_sig (signature);
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 338
Quoted 10 lines
gpg-interface.c +10
-0+if (!fmt) { +error (_ ("bad/incompatible signature '%s'" ), signature); +return -1 ; + } + +if (!strcmp (fmt->name,"ssh" )) { + status =verify_ssh_signature (sigc, fmt, payload, plen, signature, slen); + }else { + status =verify_gpg_signature (sigc, fmt, payload, plen, signature, slen); + }
OK, so get_format_by_sig() now is used to dispatch to the right backend. Which sort of makes sense, but ...
* "ssh" is the newcomer; it has no right to come before the battle-tested existing one.
* If we are dispatching via "fmt" variable, we should add fmt->verify() method to each of these formats, so that we don't have to switch based on the name.
IOW, this part should just be
fmt = get_format_by_sig(signature);
if (!fmt)
return error(_("...bad signature..."));
fmt->verify_signature(sigc, fmt, payload, plen, signature, slen);gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 343
Quoted 5 lines
gpg-interface.c +3
-0+if (status && !sigc->output) +return !!status; + status |= sigc->result !='G' ; status |= sigc->trust_level < configured_min_trust_level;
By the way, there is no verify_ssh_signature() function defined at this step [1/9], so this won't compile from the source at all. Please make sure that each step builds and passes tests.
If I were doing this patch, I probably would NOT do anything related to "ssh" in this step. Probably just doing
- rename gpg_* variables to generic names in codepaths that _will_ become generic in future steps (like "check_signature()" function);
- introduce verify_signature member to the fmt struct;
- hoist get_format_by_sig()'s callsite to check_signature() from its callee.
would be sufficient amount of work for the first step. Call that a preliminary refactoring and clean-up.
And then in the second and subsequent steps, you may start adding additional code to support ssh signing, including the new instance of fmt that has verify_ssh_signature() as its verify_signature method, etc.
Introducing ssh_allowed_signers and ssh_revocation_file at this step is way premature. Nobody uses them in this step, the code that uses them is already referenced but missing (hence the code does not build), so they are only there to frustrate readers wondering what they are for and how they will be used.
Thanks.
Re: [PATCH v3 2/9] ssh signing: add documentation
In reply to Fabian Stelzer via GitGitGadgetQuoted 13 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
Signed-off-by Fabian Stelzer <[email protected]>--- Documentation/config/gpg.txt | 35 +++++++++++++++++++++++++++++++++-- Documentation/config/user.txt | 6 ++++++ 2 files changed, 39 insertions(+), 2 deletions(-)Documentation/config/gpg.txt +0
-0
Documentation/config/gpg.txt gpg.program:: · line 14
Quoted 5 lines
Documentation/config/gpg.txt +1
-1@@ -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".
Makes sense.
Documentation/config/gpg.txt gpg.program:: · line 20
Quoted 6 lines
Documentation/config/gpg.txt +1
-1gpg.<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".
Again, makes sense.
Once the dust settles, we might want to move the hierarchy from gpg.* to a more neutral name, with proper backward compatibility migration plan, but there is no need to do so right away.
Below, I'll ask many questions. They are mostly not rhetorical and questions that you should anticipate readers of the documentation will ask (hence, you would want to update your documentation in such a way that future readers will not have to ask for clarification).
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 38
Quoted 7 lines
Documentation/config/gpg.txt +3
-0@@ -33,3 +33,34 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate` + +gpg.ssh.keyring:: + A file containing all valid SSH public signing keys.
Is "SSH public signing key" the phrase we want to use here? At first glance I mistakenly thought that I maintain a bag of my keys I will use for signing, but from the mention of "authorized keys", it apparently is the other way around, i.e. I have a bag of public keys that I can use to _verify_ signatures other people made.
What do we exactly want to convey with the phrase "all valid" to our readers? Even if I have a valid SSH key that I could sign with, if you and your project do not trust me enough, such a valid key of mine would not be in your keyring, so the phrase "all valid keys" is not all that meaningful without further qualification in the context of this sentence. A file containing ssh public keys, signatures made with which you are willing to accept, or something?
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 39
Quoted 1 line
Documentation/config/gpg.txt +1
-0+ Similar to an .ssh/authorized_keys file.
It is unclear what "similarity" is of interest here. Similar to authorized keys file, meaning that presense of this file allows holders of the listed ssh keys to remotely log-in to the repository? I somehow doubt that it is what you meant, but then ...? Did you mean "Uses the same format as .ssh/authorized_keys file" or something like that?
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 43
Quoted 4 lines
Documentation/config/gpg.txt +4
-0+ 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".
I tried to look up the "ALLOWED SIGNERS" section for details, but failed to find what "trust level" is and how trusted "fully" level is (is there higher or lower trust levels than that???). Or is the notion of "trust level" foreign to ssh signing world and the readers are expected to read this description as "listed ones are treated as having the same trust level as 'fully' trusted keys in the GPG/PGP world"?
I suspect that the section is only useful to learn the details of what the file looks like? If so, perhaps instead of saying that the keyring file looks similar to authorized-keys, be more direct and say that the keyring file uses the "ALLOWED SIGNERS" file format described in that manual page (i.e. bypassing the redirection of authorized-keys)?
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 50
Quoted 6 lines
Documentation/config/gpg.txt +6
-0+ 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.
OK.
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 52
Quoted 1 line
Documentation/config/gpg.txt +1
-0+ A repository that is only allowing signed commits can store the file
"is only allowing" -> "only allows".
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 51
Quoted 1 line
Documentation/config/gpg.txt +1
-0+ in the repository itself using a relative path.
It is unclear relative to what. Relative to the top-level of the working tree?
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 53
Quoted 2 lines
Documentation/config/gpg.txt +2
-0+ This way only committers + with an already valid key can add or change keys in the keyring.
OK.
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 60
Quoted 5 lines
Documentation/config/gpg.txt +5
-0+ 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.
All of the above unfortunately would not format correctly with multiple paragraphs. The second and subsequent paragraphs are preceded by a line with single '+' on it (instead of a blank line) and not indented.
Mimick the way the entry for "ssh.variant" uses multiple paragraphs.
Quoted 10 lines
Documentation/config/gpg.txt +5
-0+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.Documentation/config/user.txt +0
-0
Documentation/config/user.txt user.signingKey:: · line 44
Quoted 9 lines
Documentation/config/user.txt +6
-0@@ -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.
Thanks.
Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Fabian Stelzer via GitGitGadgetQuoted 3 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
calls ssh-add -L and uses the first key
Documentation/SubmittingPatches::[[describe-changes]].
Quoted 10 lines
Signed-off-by Fabian Stelzer <[email protected]>--- gpg-interface.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+)gpg-interface.c +0
-0
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 471
Quoted 5 lines
gpg-interface.c +2
-0@@ -467,6 +467,23 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +/* Returns the first public key from an ssh-agent to use for signing */ +static char *get_default_ssh_signing_key (void ) {
Style. Open and close braces around a function sit on their own lines by themselves.
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 475
Quoted 4 lines
gpg-interface.c +4
-0+struct child_process ssh_add =CHILD_PROCESS_INIT ; +int ret =-1 ; +struct strbuf key_stdout =STRBUF_INIT ; +struct strbuf **keys;
Whose releasing the resource held by "keys" when we return?
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 478
Quoted 2 lines
gpg-interface.c +2
-0+strvec_pushl (&ssh_add.args,"ssh-add" ,"-L" ,NULL ); + ret =pipe_command (&ssh_add,NULL ,0 , &key_stdout,0 ,NULL ,0 );
I often load about half a dozen keys to my ssh-agent so "ssh-add -L" will give me multi-line output. I know you wrote "the first public key" above, but that does not mean users who needs to have multiple keys can be limited to use only the first key for signing. There should be a way to say "I may have many keys for other reasons, but for signing I want to use this key, not the other ones".
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 480
Quoted 2 lines
gpg-interface.c +2
-0+if (!ret) { + keys =strbuf_split_max (&key_stdout,'\n' ,2 );
Let's not use strbuf_split_*() that is a horribly wrong interface. You do not want a set of elastic buffer after splitting. You only are peeking the first line, no? You are leaking keys[] array and probably keys[1], too.
eol = strchrnul(key_stdout.buf, '\n'); strbuf_setlen(&key_stdout, eol - key_stdout.buf);
or something along that line, perhaps?
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 487
Quoted 7 lines
gpg-interface.c +6
-0+if (keys[0 ]) +return strbuf_detach (keys[0 ],NULL ); + } + +return "" ; +}const char *get_signing_key (void )
Missing blank line after the function body.
Quoted 2 lines
gpg-interface.c +0
-0{if (configured_signing_key)
Re: [PATCH v3 4/9] ssh signing: sign using either gpg or ssh keys
In reply to Fabian Stelzer via GitGitGadgetQuoted 5 lines · Fabian Stelzer via GitGitGadget
int
sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key){gpg-interface.c +1
-1-struct child_process gpg =CHILD_PROCESS_INIT ; +struct child_process signer =CHILD_PROCESS_INIT ;int ret;
gpg-interface.c const char *get_ssh_allowed_signers(void) · line 516
Quoted 10 lines
gpg-interface.c +5
-3size_t i, j, bottom; -struct strbuf gpg_status =STRBUF_INIT ; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *temp =NULL , *buffer_file =NULL ; +char *ssh_signing_key_file =NULL ; +struct strbuf ssh_signature_filename =STRBUF_INIT ; - strvec_pushl(&gpg.args, - use_format->program, +if (!strcmp (use_format->name,"ssh" )) {
I wonder if we can split the body of these if/else clauses into separate helper functions, point them with fmt structure and dispatch via use_format->sign_buffer pointer, just like I suggested how to do the same on the signature validation side.
Quoted 72 lines
gpg-interface.c +58
-4+if (!signing_key || signing_key[0 ] =='\0' ) +return error (_ ("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) +return error_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) +return error_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; -/* * When the username signingkey is bad, program could be terminated * because gpg exits without reading and then write gets SIGPIPE. */ 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 ); + } +
gpg-interface.c const char *get_ssh_allowed_signers(void) · line 592
Quoted 10 lines
gpg-interface.c +7
-2+ 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)" ));
This looks iffy. You do call error() to show the error message, but you do not set "ret", which affects how the return value from the function is computed at the end of the function.
gpg-interface.c const char *get_ssh_allowed_signers(void)
Quoted 8 lines
gpg-interface.c +5
-0+ } + }else { + ret |= !strstr (signer_stderr.buf,"\n[GNUPG:] SIG_CREATED " ); + } +strbuf_release (&signer_stderr);if (ret)return error (_ ("gpg failed to sign the data" ));
And this error message belongs to the GPG half of the logic, not ssh (you are allowed to have a separate "ssh failed to sign" message, of course, but the point is that the error message emission should happen in the codepath dispatched for each crypto backend.
And of course, again the "if (ssh) {do this shiny new ssh thing} else {do gpg thing}" structure is questionable. We should be dispatching with use_format->fn (whatever the method name is), no?
THanks.
Re: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys
In reply to Junio C HamanoQuoted 5 lines · Junio C Hamano
Quoted 2 lines · Fabian Stelzer via GitGitGadget
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email signing/encryption and ssh keys which i think is quite common
Upcase "I".
Also: s/environemnt/environment/
Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Junio C HamanoQuoted 7 lines · Junio C Hamano
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 471
Quoted 4 lines
calls ssh-add -L and uses the first key
gpg-interface.c +2
-0+/* Returns the first public key from an ssh-agent to use for signing */ +static char *get_default_ssh_signing_key (void ) {Style. Open and close braces around a function sit on their own lines by themselves.
I recommend using clang-format (there is a config file checked into the tree) which handles most formatting conventions automatically.
Signature · 11 lines
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
Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent
Quoted 8 lines · Han-Wen Nienhuys
Quoted 5 lines · Junio C Hamano
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 471
Quoted 3 lines
calls ssh-add -L and uses the first key
gpg-interface.c +2
-0+/* Returns the first public key from an ssh-agent to use for signing */ +static char *get_default_ssh_signing_key (void ) {Style. Open and close braces around a function sit on their own lines by themselves.
I recommend using clang-format (there is a config file checked into the tree) which handles most formatting conventions automatically.
Thanks a lot.
This should really be in the in the CodingGuidelines and the MyFirstContribution docs.
Especially the clang-format-diff line from its help
"git diff -U0 --no-color --relative HEAD^ | clang-format-diff -p1 -i" is incredibly useful. Otherwise people will reformat all the things ^^
Re: [PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Junio C HamanoQuoted 30 lines · Junio C Hamano
Quoted 3 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
calls ssh-add -L and uses the first key
Documentation/SubmittingPatches::[[describe-changes]].
Quoted 10 lines
Signed-off-by Fabian Stelzer <[email protected]>--- gpg-interface.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+)gpg-interface.c +0
-0gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 471
Quoted 5 lines
gpg-interface.c +2
-0@@ -467,6 +467,23 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +/* Returns the first public key from an ssh-agent to use for signing */ +static char *get_default_ssh_signing_key (void ) {Style. Open and close braces around a function sit on their own lines by themselves.
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 475
Quoted 4 lines
gpg-interface.c +4
-0+struct child_process ssh_add =CHILD_PROCESS_INIT ; +int ret =-1 ; +struct strbuf key_stdout =STRBUF_INIT ; +struct strbuf **keys;Whose releasing the resource held by "keys" when we return?
Quoted 8 lines
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 478
Quoted 2 lines
gpg-interface.c +2
-0+strvec_pushl (&ssh_add.args,"ssh-add" ,"-L" ,NULL ); + ret =pipe_command (&ssh_add,NULL ,0 , &key_stdout,0 ,NULL ,0 );I often load about half a dozen keys to my ssh-agent so "ssh-add -L" will give me multi-line output. I know you wrote "the first public key" above, but that does not mean users who needs to have multiple keys can be limited to use only the first key for signing. There should be a way to say "I may have many keys for other reasons, but for signing I want to use this key, not the other ones".
I will make the commit message clearer. This function only provides a default key in case no key is configured in user.signingkey. If you set user.signingkey to a public key the correct private key from your agent will be used for signing.
Quoted 8 lines
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 480
Quoted 3 lines
gpg-interface.c +2
-0+if (!ret) { + keys =strbuf_split_max (&key_stdout,'\n' ,2 );Let's not use strbuf_split_*() that is a horribly wrong interface. You do not want a set of elastic buffer after splitting. You only are peeking the first line, no? You are leaking keys[] array and probably keys[1], too.
Quoted 4 lines
eol = strchrnul(key_stdout.buf, '\n'); strbuf_setlen(&key_stdout, eol - key_stdout.buf);or something along that line, perhaps?
I have changed it to what you suggested. I'm always a bit hesitant to use arithmetic with string pointers. I know its simple and efficient, but IMHO can be hard to read.
Quoted 12 lines
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 487
Quoted 8 lines
gpg-interface.c +6
-0+if (keys[0 ]) +return strbuf_detach (keys[0 ],NULL ); + } + +return "" ; +}const char *get_signing_key (void )Missing blank line after the function body.
Quoted 2 lines
gpg-interface.c +0
-0{if (configured_signing_key)
Signature · 5 lines
GIGACODES GmbH | Dr. Hermann-Neubauer-Ring 32 | D-63500 Seligenstadt www.gigacodes.de | [email protected] Phone +49 6182 8955-114 | Fax +49 6182 8955-299 | HRB 40711 AG Offenbach a. Main Geschäftsführer: Fabian Stelzer | Umsatzsteuer-ID DE219379936
Re: [PATCH v3 1/9] Add commit, tag & push signing via SSH keys
In reply to Junio C HamanoQuoted 6 lines · Junio C Hamano
Quoted 2 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
Subject: [PATCH v3 1/9] Add commit, tag & push signing via SSH keysIf you chose "ssh signing:" as the common prefix for the series, use it consistently with this step, too.
done
Quoted 13 lines
Quoted 7 lines
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.
OK.
Quoted 4 lines
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.
Quoted 8 lines
Drop this paragraph or at least tone it down. It may hold true only around your immediate circle but it is far from clear and obvious. I'd expect more people push over https:// than ssh://.
We do not really require a new feature to make much more accessible for wide average user---making it just a bit more accessible to folks in your immediate circle is perfectly fine, as long as you are not harming other people ;-)
i will redo the first commit with your suggestions from below only doing preperation for the upcoming change and then rewrite the commit message to reflect this as well.
Quoted 44 lines
Quoted 3 lines
In our corporate environemnt we use PIV x509 Certs on Yubikeys for email signing/encryption and ssh keys which i think is quite common
Upcase "I".
Quoted 8 lines
(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.All of the above promises a wonderful new world, but what is left unclear is with this step alone how much of the new world we already gain. When you ask others to read and understand your code, please give them a bit more hint to guide them what to expect and where you are taking them next.
Quoted 5 lines
fmt-merge-msg.c +0
-0fmt-merge-msg.c static void fmt_merge_msg_sigs(struct strbuf *out) · line 533
Quoted 9 lines
fmt-merge-msg.c +2
-2@@ -527,10 +527,10 @@ static void fmt_merge_msg_sigs(struct strbuf *out) len = payload.len;if (check_signature (payload.buf, payload.len, sig.buf, sig.len, &sigc) && - !sigc.gpg_output) + !sigc.output)strbuf_addstr (&sig,"gpg verification failed.\n" );else -strbuf_addstr (&sig, sigc.gpg_output); +strbuf_addstr (&sig, sigc.output);These are "rename some fields for consistency" the proposed log message promised. Makes sense, as you are taking the sigc structure away from pgp/gpg dependency.
Quoted 7 lines
gpg-interface.c +0
-0@@ -8,6 +8,7 @@#include "tempfile.h"
Quoted 3 lines
gpg-interface.c line 11
Quoted 2 lines
gpg-interface.c +1
-0static char *configured_signing_key; +const char *ssh_allowed_signers, *ssh_revocation_file;Very likely these want to be file-scope statics?
true
Quoted 147 lines
gpg-interface.c static const char *x509_sigs[] = { · line 41
Quoted 11 lines
gpg-interface.c +3
-0static enum signature_trust_level configured_min_trust_level =TRUST_UNDEFINED ;struct gpg_format {@@ -35,6 +36,14 @@ static const char *x509_sigs[] = { NULL }; +static const char *ssh_verify_args[] = { +NULL +};A blank line is missing from here.
Quoted 29 lines
gpg-interface.c +9
-1+static const char *ssh_sigs[] = { +"-----BEGIN SSH SIGNATURE-----" , +NULL +}; +static struct gpg_format gpg_format[] = { { .name ="openpgp" , .program ="gpg" , .verify_args = openpgp_verify_args,@@ -44,6 +53,9 @@ static struct gpg_format gpg_format[] = { .verify_args = x509_verify_args, .sigs = x509_sigs }, + { .name = "ssh", .program = "ssh-keygen", + .verify_args = ssh_verify_args, + .sigs = ssh_sigs }, };static struct gpg_format *use_format = &gpg_format[0 ];@@ -72,7 +84,7 @@ static struct gpg_format *get_format_by_sig(const char *sig)void signature_check_clear (struct signature_check *sigc) {FREE_AND_NULL (sigc->payload); -FREE_AND_NULL (sigc->gpg_output); +FREE_AND_NULL (sigc->output);FREE_AND_NULL (sigc->gpg_status);FREE_AND_NULL (sigc->signer);FREE_AND_NULL (sigc->key);@@ -257,16 +269,15 @@ error:FREE_AND_NULL (sigc->key);gpg-interface.c error: · line 275
Quoted 10 lines
gpg-interface.c +3
-4} -static int verify_signed_buffer (const char *payload,size_t payload_size, -const char *signature,size_t signature_size, -struct strbuf *gpg_output, -struct strbuf *gpg_status) +static int verify_gpg_signature (struct signature_check *sigc,struct gpg_format *fmt, +const char *payload,size_t payload_size, +const char *signature,size_t signature_size) {What is this hunk about? The more generic name "verify-signed-buffer" is rescinded and gets replaced by a more GPG/PGP specific helper?
You'd need to help readers a bit more by explaining in the proposed log message that you shifted the boundary of responsibility between
check_signature()and verify_signed_buffer()---it used to be that the latter inspected the signed payload to see if it a valid GPG/PGP signature before doing GPG specific validation, but you want to make the former responsible for callingget_format_by_sig(), so that you can dispatch a totally new backend that sits next to this GPG specific one.Quoted 20 lines
gpg-interface.c +2
-6struct child_process gpg =CHILD_PROCESS_INIT ; -struct gpg_format *fmt;struct tempfile *temp;int ret; -struct strbuf buf =STRBUF_INIT ; +struct strbuf gpg_out =STRBUF_INIT ; +struct strbuf gpg_err =STRBUF_INIT ; temp =mks_tempfile_t (".git_vtag_tmpXXXXXX" );if (!temp)@@ -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);gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 298
Quoted 10 lines
gpg-interface.c +3
-6strvec_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 );What is going on around here? Ahh, an unnecessary indentation change is fooling the diff and made the patch unreadable. Sigh...
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 302
Quoted 5 lines
gpg-interface.c +2
-2sigchain_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 );What is this change about? Is it another unnecessary indentation change? Please make sure you keep distraction to your readers to the minimum.
Quoted 21 lines
gpg-interface.c +2
-10@@ -309,35 +319,36 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc) { -struct strbuf gpg_output =STRBUF_INIT ; -struct strbuf gpg_status =STRBUF_INIT ; +struct gpg_format *fmt;int status; sigc->result ='N' ; sigc->trust_level =-1 ; - status =verify_signed_buffer (payload, plen, signature, slen, - &gpg_output, &gpg_status); -if (status && !gpg_output.len) - goto out; - sigc->payload =xmemdupz (payload, plen); - sigc->gpg_output =strbuf_detach (&gpg_output,NULL ); - sigc->gpg_status =strbuf_detach (&gpg_status,NULL ); -parse_gpg_output (sigc); + fmt =get_format_by_sig (signature);gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 338
Quoted 10 lines
gpg-interface.c +10
-0+if (!fmt) { +error (_ ("bad/incompatible signature '%s'" ), signature); +return -1 ; + } + +if (!strcmp (fmt->name,"ssh" )) { + status =verify_ssh_signature (sigc, fmt, payload, plen, signature, slen); + }else { + status =verify_gpg_signature (sigc, fmt, payload, plen, signature, slen); + }OK, so
get_format_by_sig()now is used to dispatch to the right backend. Which sort of makes sense, but ...* "ssh" is the newcomer; it has no right to come before the battle-tested existing one.
* If we are dispatching via "fmt" variable, we should add
fmt->verify()method to each of these formats, so that we don't have to switch based on the name.
Quoted 6 lines
IOW, this part should just be
fmt = get_format_by_sig(signature); if (!fmt) return error(_("...bad signature...")); fmt->verify_signature(sigc, fmt, payload, plen, signature, slen);
i did put ssh first to keep the default with gpg and only needing to match the new format. But the fmt->fn variant is much better. For signing as well.
Quoted 9 lines
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 343
Quoted 6 lines
gpg-interface.c +3
-0+if (status && !sigc->output) +return !!status; + status |= sigc->result !='G' ; status |= sigc->trust_level < configured_min_trust_level;By the way, there is no
verify_ssh_signature()function defined at this step [1/9], so this won't compile from the source at all. Please make sure that each step builds and passes tests.
I was thinking about this one when i split up the patch. I was not sure if that was required and didn't want to add synthetic changes (that would only appear between the diffs) just for the split.
Quoted 28 lines
If I were doing this patch, I probably would NOT do anything related to "ssh" in this step. Probably just doing
- rename gpg_* variables to generic names in codepaths that _will_ become generic in future steps (like "
check_signature()" function);- introduce
verify_signaturemember to the fmt struct;- hoist get_format_by_sig()'s callsite to
check_signature()from its callee.would be sufficient amount of work for the first step. Call that a preliminary refactoring and clean-up.
And then in the second and subsequent steps, you may start adding additional code to support ssh signing, including the new instance of fmt that has
verify_ssh_signature()as itsverify_signaturemethod, etc.Introducing
ssh_allowed_signersandssh_revocation_fileat this step is way premature. Nobody uses them in this step, the code that uses them is already referenced but missing (hence the code does not build), so they are only there to frustrate readers wondering what they are for and how they will be used.Thanks.
Re: [PATCH v3 4/9] ssh signing: sign using either gpg or ssh keys
In reply to Junio C HamanoQuoted 14 lines · Junio C Hamano
Quoted 12 lines · Fabian Stelzer via GitGitGadget
int
sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key){gpg-interface.c +5
-2-struct child_process gpg =CHILD_PROCESS_INIT ; +struct child_process signer =CHILD_PROCESS_INIT ;int ret;size_t i, j, bottom; -struct strbuf gpg_status =STRBUF_INIT ; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *temp =NULL , *buffer_file =NULL ; +char *ssh_signing_key_file =NULL ; +struct strbuf ssh_signature_filename =STRBUF_INIT ;
Quoted 7 lines
gpg-interface.c const char *get_ssh_allowed_signers(void) · line 516
Quoted 3 lines
gpg-interface.c +1
-2- strvec_pushl(&gpg.args, - use_format->program, +if (!strcmp (use_format->name,"ssh" )) {I wonder if we can split the body of these if/else clauses into separate helper functions, point them with fmt structure and dispatch via
use_format->sign_bufferpointer, just like I suggested how to do the same on the signature validation side.
yes, i like the idea and will do that.
Quoted 78 lines
Quoted 78 lines
gpg-interface.c +62
-4+if (!signing_key || signing_key[0 ] =='\0' ) +return error (_ ("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) +return error_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) +return error_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; -/* * When the username signingkey is bad, program could be terminated * because gpg exits without reading and then write gets SIGPIPE. */ 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);
Quoted 8 lines
gpg-interface.c const char *get_ssh_allowed_signers(void) · line 592
Quoted 5 lines
gpg-interface.c +3
-2- 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)" ));This looks iffy. You do call
error()to show the error message, but you do not set "ret", which affects how the return value from the function is computed at the end of the function.
I can of course fix the ret logic, but i'm not happy with this check in general either :/
The problem is that ssh-keygen seems to give different error messages especially in between the versions when the command was added (8.1 -> 8.2) and mac os x has one of those by default. The check in the t/lib-gpg.sh is much safer, but requires an additional call to ssh-keygen which i wanted to avoid here.
Quoted 13 lines
gpg-interface.c const char *get_ssh_allowed_signers(void) · line 599
Quoted 8 lines
gpg-interface.c +5
-0+ } + }else { + ret |= !strstr (signer_stderr.buf,"\n[GNUPG:] SIG_CREATED " ); + } +strbuf_release (&signer_stderr);if (ret)return error (_ ("gpg failed to sign the data" ));And this error message belongs to the GPG half of the logic, not ssh (you are allowed to have a separate "ssh failed to sign" message, of course, but the point is that the error message emission should happen in the codepath dispatched for each crypto backend.
Quoted 3 lines
And of course, again the "if (ssh) {do this shiny new ssh thing} else {do gpg thing}" structure is questionable. We should be dispatching with
use_format->fn(whatever the method name is), no?
I will rewrite this with the fmt->fn logic.
Quoted 3 lines
THanks.
Signature · 5 lines
GIGACODES GmbH | Dr. Hermann-Neubauer-Ring 32 | D-63500 Seligenstadt www.gigacodes.de | [email protected] Phone +49 6182 8955-114 | Fax +49 6182 8955-299 | HRB 40711 AG Offenbach a. Main Geschäftsführer: Fabian Stelzer | Umsatzsteuer-ID DE219379936
Re: [PATCH v3 2/9] ssh signing: add documentation
In reply to Junio C HamanoQuoted 52 lines · Junio C Hamano
Quoted 13 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
Signed-off-by Fabian Stelzer <[email protected]>--- Documentation/config/gpg.txt | 35 +++++++++++++++++++++++++++++++++-- Documentation/config/user.txt | 6 ++++++ 2 files changed, 39 insertions(+), 2 deletions(-)Documentation/config/gpg.txt +0
-0Documentation/config/gpg.txt gpg.program:: · line 14
Quoted 5 lines
Documentation/config/gpg.txt +1
-1@@ -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".Makes sense.
Documentation/config/gpg.txt gpg.program:: · line 20
Quoted 6 lines
Documentation/config/gpg.txt +1
-1gpg.<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".Again, makes sense.
Once the dust settles, we might want to move the hierarchy from gpg.* to a more neutral name, with proper backward compatibility migration plan, but there is no need to do so right away.
Below, I'll ask many questions. They are mostly not rhetorical and questions that you should anticipate readers of the documentation will ask (hence, you would want to update your documentation in such a way that future readers will not have to ask for clarification).
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 38
Quoted 7 lines
Documentation/config/gpg.txt +3
-0@@ -33,3 +33,34 @@ gpg.minTrustLevel:: * `marginal` * `fully` * `ultimate` + +gpg.ssh.keyring:: + A file containing all valid SSH public signing keys.Is "SSH public signing key" the phrase we want to use here? At first glance I mistakenly thought that I maintain a bag of my keys I will use for signing, but from the mention of "authorized keys", it apparently is the other way around, i.e. I have a bag of public keys that I can use to _verify_ signatures other people made.
Quoted 7 lines
What do we exactly want to convey with the phrase "all valid" to our readers? Even if I have a valid SSH key that I could sign with, if you and your project do not trust me enough, such a valid key of mine would not be in your keyring, so the phrase "all valid keys" is not all that meaningful without further qualification in the context of this sentence. A file containing ssh public keys, signatures made with which you are willing to accept, or something?
maybe keeeping the name "allowedSignersFile" like its called in the ssh manpage will make this clearer without needing a lot of extra explanation? The keyring name was suggested earlier to make this consistent with gpg. But it really is something different from a gpg keyring.
Quoted 8 lines
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 39
Quoted 2 lines
Documentation/config/gpg.txt +1
-0+ Similar to an .ssh/authorized_keys file.It is unclear what "similarity" is of interest here. Similar to authorized keys file, meaning that presense of this file allows holders of the listed ssh keys to remotely log-in to the repository? I somehow doubt that it is what you meant, but then ...? Did you mean "Uses the same format as .ssh/authorized_keys file" or something like that?
I meant that the format is really similar but i see the problem. I wanted to explain the format (which pretty much is just one ssh pubkey per line with a name prefixed to identify the key with) so that users don't have to look into the ssh manpage for a basic example. Maybe just provide a short example of the file contents?
Quoted 12 lines
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 43
Quoted 5 lines
Documentation/config/gpg.txt +4
-0+ 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".I tried to look up the "ALLOWED SIGNERS" section for details, but failed to find what "trust level" is and how trusted "fully" level is (is there higher or lower trust levels than that???). Or is the notion of "trust level" foreign to ssh signing world and the readers are expected to read this description as "listed ones are treated as having the same trust level as 'fully' trusted keys in the GPG/PGP world"?
SSH has nothing compared to the gpg trust levels. Your key is either in the allowed signers file or it is not. However even if it is not in the file then the signature might still be "Good" but has no matching principal to it. To be able to differentiate the two "Good" cases i used the existing gpg trust levels. This way if you set gpg.mintrustlevel = fully then the signatures with no matching key in the allowed signers file will fail to verify. Otherwise they will verify but show a message that no principal matched with this key.
Quoted 37 lines
I suspect that the section is only useful to learn the details of what the file looks like? If so, perhaps instead of saying that the keyring file looks similar to authorized-keys, be more direct and say that the keyring file uses the "ALLOWED SIGNERS" file format described in that manual page (i.e. bypassing the redirection of authorized-keys)?
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 50
Quoted 6 lines
Documentation/config/gpg.txt +6
-0+ 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.OK.
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 52
Quoted 1 line
Documentation/config/gpg.txt +1
-0+ A repository that is only allowing signed commits can store the file"is only allowing" -> "only allows".
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 51
Quoted 1 line
Documentation/config/gpg.txt +1
-0+ in the repository itself using a relative path.It is unclear relative to what. Relative to the top-level of the working tree?
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 53
Quoted 2 lines
Documentation/config/gpg.txt +2
-0+ This way only committers + with an already valid key can add or change keys in the keyring.OK.
Documentation/config/gpg.txt gpg.minTrustLevel:: · line 60
Quoted 5 lines
Documentation/config/gpg.txt +5
-0+ 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.All of the above unfortunately would not format correctly with multiple paragraphs. The second and subsequent paragraphs are preceded by a line with single '+' on it (instead of a blank line) and not indented.
Quoted 1 line
Mimick the way the entry for "ssh.variant" uses multiple paragraphs.
I'll take a look, thanks.
Quoted 21 lines
Quoted 11 lines
Documentation/config/gpg.txt +5
-0+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.Documentation/config/user.txt +0
-0Documentation/config/user.txt user.signingKey:: · line 44
Quoted 9 lines
Documentation/config/user.txt +6
-0@@ -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.Thanks.
Signature · 5 lines
GIGACODES GmbH | Dr. Hermann-Neubauer-Ring 32 | D-63500 Seligenstadt www.gigacodes.de | [email protected] Phone +49 6182 8955-114 | Fax +49 6182 8955-299 | HRB 40711 AG Offenbach a. Main Geschäftsführer: Fabian Stelzer | Umsatzsteuer-ID DE219379936
Re: [PATCH v3 2/9] ssh signing: add documentation
Quoted 1 line · Fabian Stelzer
I meant that the format is really similar but i see the problem. I wanted to explain the format (which pretty much is just one ssh pubkey per line with a name prefixed to identify the key with) so that users don't have to look into the ssh manpage for a basic example. Maybe just provide a short example of the file contents?
You can write full format description in git-allowedsigners(5) manpage, along with examples of course.
Signature · 1 line
An old man doll... just what I always wanted! - Clara
Re: [PATCH v3 2/9] ssh signing: add documentation
In reply to Fabian StelzerQuoted 5 lines · Fabian Stelzer
Quoted 2 lines
Quoted 2 lines
+gpg.ssh.keyring::
...maybe keeeping the name "allowedSignersFile" like its called in the ssh manpage will make this clearer without needing a lot of extra explanation?
Yup, that sounds like an excellent way to present this to our users.
Quoted 8 lines
SSH has nothing compared to the gpg trust levels. Your key is either in the allowed signers file or it is not. However even if it is not in the file then the signature might still be "Good" but has no matching principal to it. To be able to differentiate the two "Good" cases i used the existing gpg trust levels. This way if you set gpg.mintrustlevel = fully then the signatures with no matching key in the allowed signers file will fail to verify. Otherwise they will verify but show a message that no principal matched with this key.
Sounds sensible. Our task is to make sure that readers (not me, who have already been spoon-fed the answer by you just now) would reach the above understanding by just reading what we put in the documentation.
Thanks.
Re: [PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian Stelzer via GitGitGadgetgpg-interface.c error: · line 362
Quoted 8 lines · Fabian Stelzer via GitGitGadget
gpg-interface.c +8
-0+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.
Re: [PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures
Quoted 13 lines · Gwyneth Morgan
gpg-interface.c error: · line 362
Quoted 8 lines · Fabian Stelzer via GitGitGadget
gpg-interface.c +8
-0+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".
Quoted 2 lines
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.
v4 cover ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen
In reply to Fabian Stelzer via GitGitGadgetI 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.
There is a patch in the pipeline for openssh by Damien Miller that 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
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
another thing we could add later (via a config switch) is to use the committer email as principal, instead of looking it up with the key that was used to sign, to allow only specific trusted keys per committer.
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 | 485 +++++++++++++++++++++++++++---- gpg-interface.h | 8 +- log-tree.c | 8 +- pretty.c | 4 +- send-pack.c | 8 +- t/lib-gpg.sh | 27 ++ t/t4202-log.sh | 23 ++ t/t5534-push-signed.sh | 101 +++++++ t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++ t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++ 14 files changed, 1211 insertions(+), 65 deletions(-) create mode 100755 t/t7031-verify-tag-signed-ssh.sh create mode 100755 t/t7527-signed-commit-ssh.sh
base-commit: 75ae10bc75336db031ee58d13c5037b929235912
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v4
Pull-Request: https://github.com/git/git/pull/1041
Range-diff vs v3:
1: 390a8f816cd ! 1: b4b0e2bac1c Add commit, tag & push signing via SSH keys
@@ Metadata
Author: Fabian Stelzer <[email protected]>
## Commit message ##
- Add commit, tag & push signing via SSH keys
+ ssh signing: preliminary refactoring and clean-up
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
+ 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
@@ Commit message
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 <[email protected]>
## fmt-merge-msg.c ##
@@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)
+ buf = payload.buf;
len = payload.len;
if (check_signature(payload.buf, payload.len, sig.buf,
- sig.len, &sigc) &&
+- sig.len, &sigc) &&
- !sigc.gpg_output)
-+ !sigc.output)
++ sig.len, &sigc) &&
++ !sigc.output)
strbuf_addstr(&sig, "gpg verification failed.\n");
else
- strbuf_addstr(&sig, sigc.gpg_output);
@@ fmt-merge-msg.c: static void fmt_merge_msg_sigs(struct strbuf *out)
## gpg-interface.c ##
-@@
- #include "tempfile.h"
-
- static char *configured_signing_key;
-+const char *ssh_allowed_signers, *ssh_revocation_file;
- static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
+@@ gpg-interface.c: struct gpg_format {
+ const char *program;
+ const char **verify_args;
+ const char **sigs;
++ int (*verify_signed_buffer)(struct signature_check *sigc,
++ struct gpg_format *fmt, const char *payload,
++ size_t payload_size, const char *signature,
++ size_t signature_size);
++ int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,
++ const char *signing_key);
+ };
- struct gpg_format {
+ static const char *openpgp_verify_args[] = {
@@ gpg-interface.c: static const char *x509_sigs[] = {
NULL
};
-+static const char *ssh_verify_args[] = {
-+ NULL
-+};
-+static const char *ssh_sigs[] = {
-+ "-----BEGIN SSH SIGNATURE-----",
-+ NULL
-+};
++static int verify_gpg_signed_buffer(struct signature_check *sigc,
++ struct gpg_format *fmt, const char *payload,
++ size_t payload_size, const char *signature,
++ size_t signature_size);
++static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
++ const char *signing_key);
+
static struct gpg_format gpg_format[] = {
- { .name = "openpgp", .program = "gpg",
- .verify_args = openpgp_verify_args,
-@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
- .verify_args = x509_verify_args,
- .sigs = x509_sigs
+- { .name = "openpgp", .program = "gpg",
+- .verify_args = openpgp_verify_args,
+- .sigs = openpgp_sigs
++ {
++ .name = "openpgp",
++ .program = "gpg",
++ .verify_args = openpgp_verify_args,
++ .sigs = openpgp_sigs,
++ .verify_signed_buffer = verify_gpg_signed_buffer,
++ .sign_buffer = sign_buffer_gpg,
+ },
+- { .name = "x509", .program = "gpgsm",
+- .verify_args = x509_verify_args,
+- .sigs = x509_sigs
++ {
++ .name = "x509",
++ .program = "gpgsm",
++ .verify_args = x509_verify_args,
++ .sigs = x509_sigs,
++ .verify_signed_buffer = verify_gpg_signed_buffer,
++ .sign_buffer = sign_buffer_gpg,
},
-+ { .name = "ssh", .program = "ssh-keygen",
-+ .verify_args = ssh_verify_args,
-+ .sigs = ssh_sigs },
};
- static struct gpg_format *use_format = &gpg_format[0];
@@ gpg-interface.c: static struct gpg_format *get_format_by_sig(const char *sig)
void signature_check_clear(struct signature_check *sigc)
{
@@ gpg-interface.c: error:
- const char *signature, size_t signature_size,
- struct strbuf *gpg_output,
- struct strbuf *gpg_status)
-+static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,
-+ const char *payload, size_t payload_size,
-+ const char *signature, size_t signature_size)
++static int verify_gpg_signed_buffer(struct signature_check *sigc,
++ struct gpg_format *fmt, const char *payload,
++ size_t payload_size, const char *signature,
++ size_t signature_size)
{
struct child_process gpg = CHILD_PROCESS_INIT;
- struct gpg_format *fmt;
struct tempfile *temp;
int ret;
- struct strbuf buf = STRBUF_INIT;
-+ struct strbuf gpg_out = STRBUF_INIT;
-+ struct strbuf gpg_err = STRBUF_INIT;
++ struct strbuf gpg_stdout = STRBUF_INIT;
++ struct strbuf gpg_stderr = STRBUF_INIT;
temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
if (!temp)
@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay
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);
--
+@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t payload_size,
+ "--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);
++ ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,
++ &gpg_stderr, 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);
+ 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);
+
-+ delete_tempfile(&temp);
-+ strbuf_release(&gpg_out);
-+ strbuf_release(&gpg_err);
++ strbuf_release(&gpg_stdout);
++ strbuf_release(&gpg_stderr);
return ret;
}
@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay
- sigc->gpg_status = strbuf_detach(&gpg_status, NULL);
- parse_gpg_output(sigc);
+ fmt = get_format_by_sig(signature);
-+ if (!fmt) {
-+ error(_("bad/incompatible signature '%s'"), signature);
-+ return -1;
-+ }
++ if (!fmt)
++ return error(_("bad/incompatible signature '%s'"), signature);
++
++ status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
++ slen);
+
-+ if (!strcmp(fmt->name, "ssh")) {
-+ status = verify_ssh_signature(sigc, fmt, payload, plen, signature, slen);
-+ } else {
-+ status = verify_gpg_signature(sigc, fmt, payload, plen, signature, slen);
-+ }
+ if (status && !sigc->output)
+ return !!status;
+
@@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay
void print_signature_buffer(const struct signature_check *sigc, unsigned flags)
{
- const char *output = flags & GPG_VERIFY_RAW ?
+- const char *output = flags & GPG_VERIFY_RAW ?
- sigc->gpg_status : sigc->gpg_output;
-+ sigc->gpg_status : sigc->output;
++ const char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :
++ sigc->output;
if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
fputs(sigc->payload, stdout);
-@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
- int ret;
-
- if (!strcmp(var, "user.signingkey")) {
-+ /*
-+ * user.signingkey can contain one of the following
-+ * when format = openpgp/x509
-+ * - GPG KeyID
-+ * when format = ssh
-+ * - literal ssh public key (e.g. ssh-rsa XXXKEYXXX comment)
-+ * - path to a file containing a public or a private ssh key
-+ */
- if (!value)
- return config_error_nonbool(var);
- set_signing_key(value);
- return 0;
- }
-
-+ if (!strcmp(var, "gpg.ssh.keyring")) {
-+ if (!value)
-+ return config_error_nonbool(var);
-+ return git_config_string(&ssh_allowed_signers, var, value);
-+ }
-+
-+ if (!strcmp(var, "gpg.ssh.revocationkeyring")) {
-+ if (!value)
-+ return config_error_nonbool(var);
-+ return git_config_string(&ssh_revocation_file, var, value);
-+ }
-+
- if (!strcmp(var, "gpg.format")) {
- if (!value)
- return config_error_nonbool(var);
-@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
- if (!strcmp(var, "gpg.x509.program"))
- fmtname = "x509";
-
-+ if (!strcmp(var, "gpg.ssh.program"))
-+ fmtname = "ssh";
-+
- if (fmtname) {
- fmt = get_format_by_name(fmtname);
- return git_config_string(&fmt->program, var, value);
@@ gpg-interface.c: const char *get_signing_key(void)
- {
- if (configured_signing_key)
- return configured_signing_key;
-- return git_committer_info(IDENT_STRICT|IDENT_NO_DATE);
-+ if (!strcmp(use_format->name, "ssh")) {
-+ return get_default_ssh_signing_key();
-+ } else {
-+ return git_committer_info(IDENT_STRICT | IDENT_NO_DATE);
-+ }
-+}
-+
-+const char *get_ssh_allowed_signers(void)
-+{
-+ if (ssh_allowed_signers)
-+ return ssh_allowed_signers;
-+
-+ die("A Path to an allowed signers ssh keyring is needed for validation");
}
int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
++{
++ return use_format->sign_buffer(buffer, signature, signing_key);
++}
++
++static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
++ const char *signing_key)
+ {
+ struct child_process gpg = CHILD_PROCESS_INIT;
+ int ret;
## gpg-interface.h ##
@@ gpg-interface.h: enum signature_trust_level {
@@ gpg-interface.h: enum signature_trust_level {
struct signature_check {
char *payload;
- char *gpg_output;
-- char *gpg_status;
+ char *output;
-+ char *gpg_status; /* Only used internally -> remove from this public api */
+ char *gpg_status;
/*
- * possible "result":
-@@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature,
- int git_gpg_config(const char *, const char *, void *);
- void set_signing_key(const char *);
- const char *get_signing_key(void);
-+const char *get_ssh_allowed_signers(void);
- int check_signature(const char *payload, size_t plen,
- const char *signature, size_t slen,
- struct signature_check *sigc);
## log-tree.c ##
@@ log-tree.c: static void show_signature(struct rev_info *opt, struct commit *commit)
4: df55b9e1d59 ! 2: 2c75adee8e1 ssh signing: sign using either gpg or ssh keys
@@ Metadata
Author: Fabian Stelzer <[email protected]>
## Commit message ##
- ssh signing: sign using either gpg or ssh keys
+ ssh signing: add ssh signature format and signing using ssh keys
- implements the actual ssh-keygen -Y sign operation
+ 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.
- 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.
+
+ 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 <[email protected]>
## gpg-interface.c ##
-@@ gpg-interface.c: const char *get_ssh_allowed_signers(void)
+@@ gpg-interface.c: static const char *x509_sigs[] = {
+ NULL
+ };
+
++static const char *ssh_verify_args[] = { NULL };
++static const char *ssh_sigs[] = {
++ "-----BEGIN SSH SIGNATURE-----",
++ NULL
++};
++
+ static int verify_gpg_signed_buffer(struct signature_check *sigc,
+ struct gpg_format *fmt, const char *payload,
+ size_t payload_size, const char *signature,
+ size_t signature_size);
+ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
+ const char *signing_key);
++static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
++ const char *signing_key);
+
+ static struct gpg_format gpg_format[] = {
+ {
+@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
+ .verify_signed_buffer = verify_gpg_signed_buffer,
+ .sign_buffer = sign_buffer_gpg,
+ },
++ {
++ .name = "ssh",
++ .program = "ssh-keygen",
++ .verify_args = ssh_verify_args,
++ .sigs = ssh_sigs,
++ .verify_signed_buffer = NULL, /* TODO */
++ .sign_buffer = sign_buffer_ssh
++ },
+ };
+
+ static struct gpg_format *use_format = &gpg_format[0];
+@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
+ if (!strcmp(var, "gpg.x509.program"))
+ fmtname = "x509";
+
++ if (!strcmp(var, "gpg.ssh.program"))
++ fmtname = "ssh";
++
+ if (fmtname) {
+ fmt = get_format_by_name(fmtname);
+ return git_config_string(&fmt->program, var, value);
+@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
+ return use_format->sign_buffer(buffer, signature, signing_key);
+ }
- int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
++static void strbuf_trim_trailing_cr(struct strbuf *buffer, int offset)
++{
++ size_t i, j;
++
++ for (i = j = offset; i < buffer->len; i++) {
++ if (buffer->buf[i] != '\r') {
++ if (i != j)
++ buffer->buf[j] = buffer->buf[i];
++ j++;
++ }
++ }
++ strbuf_setlen(buffer, j);
++}
++
+ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
+ const char *signing_key)
{
-- struct child_process gpg = CHILD_PROCESS_INIT;
-+ struct child_process signer = CHILD_PROCESS_INIT;
+ struct child_process gpg = CHILD_PROCESS_INIT;
int ret;
- size_t i, j, bottom;
-- struct strbuf gpg_status = STRBUF_INIT;
+- size_t i, j, bottom;
++ size_t bottom;
+ struct strbuf gpg_status = STRBUF_INIT;
+
+ strvec_pushl(&gpg.args,
+@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
+ return error(_("gpg failed to sign the data"));
+
+ /* Strip CR from the line endings, in case we are on Windows. */
+- for (i = j = bottom; i < signature->len; i++)
+- if (signature->buf[i] != '\r') {
+- if (i != j)
+- signature->buf[j] = signature->buf[i];
+- j++;
+- }
+- strbuf_setlen(signature, j);
++ strbuf_trim_trailing_cr(signature, bottom);
+
+ return 0;
+ }
++
++static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
++ const char *signing_key)
++{
++ struct child_process signer = CHILD_PROCESS_INIT;
++ int ret = -1;
++ size_t bottom;
+ struct strbuf signer_stderr = STRBUF_INIT;
+ struct tempfile *temp = NULL, *buffer_file = NULL;
+ char *ssh_signing_key_file = NULL;
+ struct strbuf ssh_signature_filename = STRBUF_INIT;
-
-- strvec_pushl(&gpg.args,
-- use_format->program,
-+ if (!strcmp(use_format->name, "ssh")) {
-+ if (!signing_key || signing_key[0] == '\0')
-+ return error(_("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)
-+ return error_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)
-+ return error_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;
-+ }
++ if (!signing_key || signing_key[0] == '\0')
++ return error(
++ _("user.signingkey needs to be set for ssh signing"));
+
-+ 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);
++ if (istarts_with(signing_key, "ssh-")) {
++ /* A literal ssh key */
++ temp = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
++ if (!temp)
++ return error_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);
++ goto out;
+ }
-+ unlink_or_warn(ssh_signature_filename.buf);
-+ strbuf_release(&ssh_signature_filename);
-+ delete_tempfile(&buffer_file);
++ ssh_signing_key_file = temp->filename.buf;
+ } else {
-+ strvec_pushl(&signer.args, use_format->program ,
- "--status-fd=2",
- "-bsau", signing_key,
- NULL);
-
-- bottom = signature->len;
--
- /*
- * When the username signingkey is bad, program could be terminated
- * because gpg exits without reading and then write gets SIGPIPE.
- */
- 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);
++ /* We assume a file */
++ ssh_signing_key_file = expand_user_path(signing_key, 1);
++ }
++
++ buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
++ if (!buffer_file) {
++ error_errno(_("could not create temporary file"));
++ goto out;
++ }
++
++ if (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||
++ close_tempfile_gently(buffer_file) < 0) {
++ error_errno(_("failed writing ssh signing key buffer to '%s'"),
++ buffer_file->filename.buf);
++ goto out;
++ }
++
++ strvec_pushl(&signer.args, use_format->program, "-Y", "sign", "-n",
++ "git", "-f", ssh_signing_key_file,
++ buffer_file->filename.buf, NULL);
++
++ sigchain_push(SIGPIPE, SIG_IGN);
++ ret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);
++ sigchain_pop(SIGPIPE);
++
++ if (ret && strstr(signer_stderr.buf, "usage:")) {
++ error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
++ goto out;
++ }
++
++ if (ret) {
++ 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, 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"));
++ goto out;
++ }
++
++ /* Strip CR from the line endings, in case we are on Windows. */
++ strbuf_trim_trailing_cr(signature, bottom);
++
++out:
+ 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 ");
-+ }
++ if (buffer_file)
++ delete_tempfile(&buffer_file);
+ strbuf_release(&signer_stderr);
- if (ret)
- return error(_("gpg failed to sign the data"));
-
++ strbuf_release(&ssh_signature_filename);
++ return ret;
++}
3: b84b2812470 ! 3: 1ec5c06cbe9 ssh signing: retrieve a default key from ssh-agent
@@ Metadata
## Commit message ##
ssh signing: retrieve a default key from ssh-agent
- calls ssh-add -L and uses the first key
+ 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 <[email protected]>
@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
}
+/* Returns the first public key from an ssh-agent to use for signing */
-+static char *get_default_ssh_signing_key(void) {
++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;
@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
+ return strbuf_detach(keys[0], NULL);
+ }
+
++ strbuf_release(&key_stdout);
+ return "";
+}
++
const char *get_signing_key(void)
{
if (configured_signing_key)
+ return configured_signing_key;
+- return git_committer_info(IDENT_STRICT|IDENT_NO_DATE);
++ if (!strcmp(use_format->name, "ssh")) {
++ return get_default_ssh_signing_key();
++ } else {
++ return git_committer_info(IDENT_STRICT | IDENT_NO_DATE);
++ }
+ }
+
+ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
5: 0581c72634c ! 4: ec6931082ee ssh signing: provide a textual representation of the signing key
@@ Metadata
## Commit message ##
ssh signing: provide a textual representation of the signing key
- 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
+ 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 <[email protected]>
@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
return 0;
}
-+static char *get_ssh_key_fingerprint(const char *signing_key) {
++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;
@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
+ * 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);
++ 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);
++ strvec_pushl(&ssh_keygen.args, "ssh-keygen", "-lf",
++ configured_signing_key, NULL);
+ ret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0,
-+ NULL, 0);
++ NULL, 0);
+ }
+
+ if (!!ret)
+ die_errno(_("failed to get the ssh fingerprint for key '%s'"),
-+ signing_key);
++ 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);
++ signing_key);
+
+ return strbuf_detach(fingerprint[1], NULL);
+}
+
/* 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;
-@@ gpg-interface.c: static char *get_default_ssh_signing_key(void) {
-
+ static char *get_default_ssh_signing_key(void)
+ {
+@@ gpg-interface.c: static char *get_default_ssh_signing_key(void)
return "";
}
-+
+
+/* Returns a textual but unique representation ot the signing key */
-+const char *get_signing_key_id(void) {
++const char *get_signing_key_id(void)
++{
+ if (!strcmp(use_format->name, "ssh")) {
+ return get_ssh_key_fingerprint(get_signing_key());
+ } else {
@@ gpg-interface.h: int sign_buffer(struct strbuf *buffer, struct strbuf *signature
+ * Either a GPG KeyID or a SSH Key Fingerprint
+ */
+const char *get_signing_key_id(void);
-+
- const char *get_ssh_allowed_signers(void);
int check_signature(const char *payload, size_t plen,
const char *signature, size_t slen,
+ struct signature_check *sigc);
## send-pack.c ##
@@ send-pack.c: static int generate_push_cert(struct strbuf *req_buf,
6: 381a950a6e1 ! 5: 4436cb3a122 ssh signing: parse ssh-keygen output and verify signatures
@@ Metadata
## Commit message ##
ssh signing: parse ssh-keygen output and verify signatures
- Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) "ALLOWED
+ 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
@@ Commit message
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.
+ 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
@@ gpg-interface.c
#include "gpg-interface.h"
#include "sigchain.h"
#include "tempfile.h"
-@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,
- return 1;
+
+ static char *configured_signing_key;
++static const char *ssh_allowed_signers, *ssh_revocation_file;
+ static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
+
+ struct gpg_format {
+@@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sigc,
+ struct gpg_format *fmt, const char *payload,
+ size_t payload_size, const char *signature,
+ size_t signature_size);
++static int verify_ssh_signed_buffer(struct signature_check *sigc,
++ struct gpg_format *fmt, const char *payload,
++ size_t payload_size, const char *signature,
++ size_t signature_size);
+ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
+ const char *signing_key);
+ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
+ .program = "ssh-keygen",
+ .verify_args = ssh_verify_args,
+ .sigs = ssh_sigs,
+- .verify_signed_buffer = NULL, /* TODO */
++ .verify_signed_buffer = verify_ssh_signed_buffer,
+ .sign_buffer = sign_buffer_ssh
+ },
+ };
+@@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sigc,
+ return ret;
}
+static void parse_ssh_output(struct signature_check *sigc)
+{
-+ struct string_list parts = STRING_LIST_INIT_DUP;
-+ char *line = NULL;
++ 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
+ */
@@ gpg-interface.c: static int parse_gpg_trust_level(const char *level,
+ 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 */
++
++ if (skip_prefix(line, "Good \"git\" signature for ", &line)) {
++ /* Valid signature and known principal */
+ sigc->result = 'G';
+ sigc->trust_level = TRUST_FULLY;
-+ sigc->signer = xstrdup(parts.items[4].string);
-+ sigc->fingerprint = xstrdup(parts.items[8].string);
++
++ /* Search for the last "with" to get the full principal */
++ principal = line;
++ do {
++ search = strstr(line, " with ");
++ if (search)
++ line = search + 1;
++ } while (search != NULL);
++ sigc->signer = xmemdupz(principal, line - principal - 1);
++ sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+ sigc->key = xstrdup(sigc->fingerprint);
-+ } else if (parts.nr >= 7 && starts_with(line, "Good \"git\" signature with ")) {
++ } else if (skip_prefix(line, "Good \"git\" signature with ", &line)) {
+ /* Valid signature, but key unknown */
+ sigc->result = 'G';
+ sigc->trust_level = TRUST_UNDEFINED;
-+ sigc->fingerprint = xstrdup(parts.items[6].string);
++ sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+ 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);
++}
++
++static const char *get_ssh_allowed_signers(void)
++{
++ if (ssh_allowed_signers)
++ return ssh_allowed_signers;
+
-+ string_list_clear(&parts, 0);
-+ FREE_AND_NULL(line);
++ die("gpg.ssh.allowedSignersFile needs to be configured and exist for validation");
+}
+
- static void parse_gpg_output(struct signature_check *sigc)
- {
- const char *buf = sigc->gpg_status;
-@@ gpg-interface.c: error:
- FREE_AND_NULL(sigc->key);
- }
-
-+static int verify_ssh_signature(struct signature_check *sigc,
-+ struct gpg_format *fmt,
-+ const char *payload, size_t payload_size,
-+ const char *signature, size_t signature_size)
++static int verify_ssh_signed_buffer(struct signature_check *sigc,
++ struct gpg_format *fmt, const char *payload,
++ size_t payload_size, const char *signature,
++ size_t signature_size)
+{
+ struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+ struct tempfile *temp;
@@ gpg-interface.c: error:
+ }
+
+ /* 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)"));
++ 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+)"));
++ return ret;
+ }
+ if (ret || !ssh_keygen_out.len) {
-+ /* We did not find a matching principal in the keyring - Check without validation */
++ /* We did not find a matching principal in the allowedSigners - Check
++ * without validation */
+ child_process_init(&ssh_keygen);
-+ strvec_pushl(&ssh_keygen.args, fmt->program,
-+ "-Y", "check-novalidate",
-+ "-n", "git",
-+ "-s", temp->filename.buf,
-+ NULL);
-+ ret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);
++ 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')) {
++ for (line = ssh_keygen_out.buf; *line;
++ line = strchrnul(line + 1, '\n')) {
+ while (*line == '\n')
+ line++;
+ if (!*line)
+ break;
+
-+ trust_size = strcspn(line, " \n");
++ 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);
++ 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);
++ strvec_pushl(&ssh_keygen.args, "-r",
++ ssh_revocation_file, NULL);
+ } else {
-+ warning(_("ssh signing revocation file configured but not found: %s"), ssh_revocation_file);
++ 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);
++ &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;
@@ gpg-interface.c: error:
+ return ret;
+}
+
- static int verify_gpg_signature(struct signature_check *sigc, struct gpg_format *fmt,
- const char *payload, size_t payload_size,
- const char *signature, size_t signature_size)
+ int check_signature(const char *payload, size_t plen, const char *signature,
+ size_t slen, struct signature_check *sigc)
+ {
+@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
+ return 0;
+ }
+
++ if (!strcmp(var, "gpg.ssh.allowedsignersfile")) {
++ if (!value)
++ return config_error_nonbool(var);
++ return git_config_string(&ssh_allowed_signers, var, value);
++ }
++
++ if (!strcmp(var, "gpg.ssh.revocationFile")) {
++ if (!value)
++ return config_error_nonbool(var);
++ return git_config_string(&ssh_revocation_file, var, value);
++ }
++
+ if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
+ fmtname = "openpgp";
+
7: 1d292a8d7a2 ! 6: 06a76e64b35 ssh signing: add test prereqs
@@ Metadata
## Commit message ##
ssh signing: add test prereqs
- generate some ssh keys and a allowed keys keyring for testing
+ generate some ssh keys and a allowedSignersFile for testing
Signed-off-by: Fabian Stelzer <[email protected]>
@@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
+ 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_\" NR \" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.keyring" &&
-+ cat "${GNUPGHOME}/ssh.all_valid.keyring" &&
++ find "${GNUPGHOME}" -name *ssh_signing_key.pub -exec cat {} \; | awk "{print \"\\\"principal with number \" NR \"\\\" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
++ cat "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
+ ssh-keygen -t ed25519 -N "" -f "${GNUPGHOME}/untrusted_ssh_signing_key" >/dev/null
+'
+
@@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
+SIGNING_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key"
+SIGNING_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key"
+SIGNING_KEY_PASSPHRASE="super_secret"
-+SIGNING_KEYRING="${GNUPGHOME}/ssh.all_valid.keyring"
++SIGNING_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile"
+
+GOOD_SIGNATURE_TRUSTED='Good "git" signature for'
+GOOD_SIGNATURE_UNTRUSTED='Good "git" signature with'
8: 338d1b976e9 ! 7: 4dc5572083b ssh signing: duplicate t7510 tests for commits
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify and show signatures' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ test_config gpg.mintrustlevel UNDEFINED &&
+ (
+ for commit in initial second merge fourth-signed \
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git verify-commit eighth-signed-alt 2>actual &&
+ grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
+ ! grep "${BAD_SIGNATURE}" actual &&
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ test_config gpg.minTrustLevel fully &&
+ git verify-commit sixth-signed
+'
+
+test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ test_config gpg.minTrustLevel marginal &&
+ git verify-commit sixth-signed
+'
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify signatures with --raw' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ (
+ for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed
+ do
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'show signed commit with signature' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git show -s initial >commit &&
+ git show -s --show-signature initial >show &&
+ git verify-commit -v initial >verify.1 2>verify.2 &&
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'detect fudged signature' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git cat-file commit seventh-signed >raw &&
+ sed -e "s/^seventh/7th forged/" raw >forged1 &&
+ git hash-object -w -t commit forged1 >forged1.commit &&
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'detect fudged signature with NUL' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git cat-file commit seventh-signed >raw &&
+ cat raw >forged2 &&
+ echo Qwik | tr "Q" "\000" >>forged2 &&
@@ t/t7527-signed-commit-ssh.sh (new)
+test_expect_success GPGSSH 'amending already signed commit' '
+ test_config gpg.format ssh &&
+ test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git checkout fourth-signed^0 &&
+ git commit --amend -S --no-edit &&
+ git verify-commit HEAD &&
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'show good signature with custom format' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") &&
+ cat >expect.tmpl <<-\EOF &&
+ G
+ FINGERPRINT
-+ principal_1
++ principal with number 1
+ FINGERPRINT
+
+ EOF
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'show bad signature with custom format' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ cat >expect <<-\EOF &&
+ B
+
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'show untrusted signature with custom format' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ cat >expect.tmpl <<-\EOF &&
+ U
+ FINGERPRINT
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'show untrusted signature with undefined trust level' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ cat >expect.tmpl <<-\EOF &&
+ undefined
+ FINGERPRINT
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ cat >expect.tmpl <<-\EOF &&
+ fully
+ FINGERPRINT
-+ principal_1
++ principal with number 1
+ FINGERPRINT
+
+ EOF
@@ t/t7527-signed-commit-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ test_config log.showsignature true &&
+ git show initial >actual &&
+ grep "${GOOD_SIGNATURE_TRUSTED}" actual
9: 33330fda441 ! 8: 275dd8a1013 ssh signing: add more tests for logs, tags & push certs
@@ t/t4202-log.sh: test_expect_success GPGSM 'log OpenPGP fingerprint' '
'
+test_expect_success GPGSSH 'log ssh key fingerprint' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect &&
+ git log -n1 --format="%GF | %GP" signed-ssh >actual &&
+ test_cmp expect actual
@@ t/t4202-log.sh: test_expect_success GPGSM 'log --graph --show-signature x509' '
'
+test_expect_success GPGSSH 'log --graph --show-signature ssh' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git log --graph --show-signature -n1 signed-ssh >actual &&
+ grep "${GOOD_SIGNATURE_TRUSTED}" actual
+'
@@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certific
+test_expect_success GPGSSH 'ssh signed push sends push certificate' '
+ prepare_dst &&
+ mkdir -p dst/.git/hooks &&
-+ git -C dst config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git -C dst config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git -C dst config receive.certnonceseed sekrit &&
+ write_script dst/.git/hooks/post-receive <<-\EOF &&
+ # discard the update list
@@ t/t5534-push-signed.sh: test_expect_success GPG 'signed push sends push certific
+
+ (
+ cat <<-\EOF &&
-+ SIGNER=principal_1
++ SIGNER=principal with number 1
+ KEY=FINGERPRINT
+ STATUS=G
+ NONCE_STATUS=OK
@@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed use
+ test_config gpg.format ssh &&
+ prepare_dst &&
+ mkdir -p dst/.git/hooks &&
-+ git -C dst config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ git -C dst config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git -C dst config receive.certnonceseed sekrit &&
+ write_script dst/.git/hooks/post-receive <<-\EOF &&
+ # discard the update list
@@ t/t5534-push-signed.sh: test_expect_success GPGSM 'fail without key and heed use
+
+ (
+ cat <<-\EOF &&
-+ SIGNER=principal_1
++ SIGNER=principal with number 1
+ KEY=FINGERPRINT
+ STATUS=G
+ NONCE_STATUS=OK
@@ t/t7031-verify-tag-signed-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify and show ssh signatures' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ (
+ for tag in initial second merge fourth-signed sixth-signed seventh-signed
+ do
@@ t/t7031-verify-tag-signed-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'detect fudged ssh signature' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git cat-file tag seventh-signed >raw &&
+ sed -e "/^tag / s/seventh/7th forged/" raw >forged1 &&
+ git hash-object -w -t tag forged1 >forged1.tag &&
@@ t/t7031-verify-tag-signed-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify ssh signatures with --raw' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ (
+ for tag in initial second merge fourth-signed sixth-signed seventh-signed
+ do
@@ t/t7031-verify-tag-signed-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify signatures with --raw ssh' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ git verify-tag --raw sixth-signed 2>actual &&
+ grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
+ ! grep "${BAD_SIGNATURE}" actual &&
@@ t/t7031-verify-tag-signed-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verify multiple tags ssh' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ tags="seventh-signed sixth-signed" &&
+ for i in $tags
+ do
@@ t/t7031-verify-tag-signed-ssh.sh (new)
+'
+
+test_expect_success GPGSSH 'verifying tag with --format - ssh' '
-+ test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
++ test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+ cat >expect <<-\EOF &&
+ tagname : fourth-signed
+ EOF 2: 2f8452f6570 ! 9: 13f6c229bd1 ssh signing: add documentation
@@ Documentation/config/gpg.txt: gpg.minTrustLevel::
* fully
* ultimate
+
-+gpg.ssh.keyring::
-+ A file containing all valid SSH public signing keys.
-+ Similar to an .ssh/authorized_keys file.
++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.: [email protected],[email protected] ssh-rsa AAAAX1...
+ 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".
++ 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.
+
-+ 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::
++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
Signature · 1 line
gitgitgadget
PATCH v4 1/9 ssh signing: preliminary refactoring and clean-up
From: Fabian Stelzer <[email protected]>
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
--- 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(-)
fmt-merge-msg.c +3 -3
@@ -526,11 +526,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out) buf = payload.buf; len = payload.len;if (check_signature (payload.buf, payload.len, sig.buf, - sig.len, &sigc) && - !sigc.gpg_output) + sig.len, &sigc) && + !sigc.output)strbuf_addstr (&sig,"gpg verification failed.\n" );else -strbuf_addstr (&sig, sigc.gpg_output); +strbuf_addstr (&sig, sigc.output); }signature_check_clear (&sigc);
gpg-interface.c +64 -40
176 lines not on this page · raw message
gpg-interface.h +1 -1
@@ -17,7 +17,7 @@ enum signature_trust_level {struct signature_check {char *payload; -char *gpg_output; +char *output;char *gpg_status;/*
log-tree.c +4 -4
@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit) status =check_signature (payload.buf, payload.len, signature.buf, signature.len, &sigc); -if (status && !sigc.gpg_output) +if (status && !sigc.output)show_sig_lines (opt, status,"No signature\n" );else -show_sig_lines (opt, status, sigc.gpg_output); +show_sig_lines (opt, status, sigc.output);signature_check_clear (&sigc); out:
@@ -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);else strbuf_addstr (&verify_message,"No signature\n" );signature_check_clear (&sigc);
pretty.c +2 -2
@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */check_commit_signature (c->commit, &(c->signature_check));switch (placeholder[1 ]) {case 'G' : -if (c->signature_check.gpg_output) -strbuf_addstr (sb, c->signature_check.gpg_output); +if (c->signature_check.output) +strbuf_addstr (sb, c->signature_check.output);break ;case '?' :switch (c->signature_check.result) {
Signature · 1 line
gitgitgadget
PATCH v4 2/9 ssh signing: add ssh signature format and signing using ssh keys
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 8 deletions(-)
gpg-interface.c +129 -8
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {NULL }; +static const char *ssh_verify_args[] = {NULL }; +static const char *ssh_sigs[] = { +"-----BEGIN SSH SIGNATURE-----" , +NULL +}; +static int verify_gpg_signed_buffer (struct signature_check *sigc,struct gpg_format *fmt,const char *payload,size_t payload_size,const char *signature,size_t signature_size);static int sign_buffer_gpg (struct strbuf *buffer,struct strbuf *signature,const char *signing_key); +static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key);static struct gpg_format gpg_format[] = { {
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = { .verify_signed_buffer = verify_gpg_signed_buffer, .sign_buffer = sign_buffer_gpg, }, + { + .name = "ssh", + .program = "ssh-keygen", + .verify_args = ssh_verify_args, + .sigs = ssh_sigs, + .verify_signed_buffer = NULL, /* TODO */ + .sign_buffer = sign_buffer_ssh + }, };static struct gpg_format *use_format = &gpg_format[0 ];
@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)if (!strcmp (var,"gpg.x509.program" )) fmtname ="x509" ; +if (!strcmp (var,"gpg.ssh.program" )) + fmtname ="ssh" ; +if (fmtname) { fmt =get_format_by_name (fmtname);return git_config_string (&fmt->program, var, value);
@@ -463,12 +482,26 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigreturn use_format->sign_buffer (buffer, signature, signing_key); } +static void strbuf_trim_trailing_cr (struct strbuf *buffer,int offset) +{ +size_t i, j; + +for (i = j = offset; i < buffer->len; i++) { +if (buffer->buf[i] !='\r' ) { +if (i != j) + buffer->buf[j] = buffer->buf[i]; + j++; + } + } +strbuf_setlen (buffer, j); +} +static int sign_buffer_gpg (struct strbuf *buffer,struct strbuf *signature,const char *signing_key) {struct child_process gpg =CHILD_PROCESS_INIT ;int ret; -size_t i, j, bottom; +size_t bottom;struct strbuf gpg_status =STRBUF_INIT ; strvec_pushl(&gpg.args,
@@ -494,13 +527,101 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,return error (_ ("gpg failed to sign the data" ));/* Strip CR from the line endings, in case we are on Windows. */ -for (i = j = bottom; i < signature->len; i++) -if (signature->buf[i] !='\r' ) { -if (i != j) - signature->buf[j] = signature->buf[i]; - j++; - } -strbuf_setlen (signature, j); +strbuf_trim_trailing_cr (signature, bottom);return 0 ; } + +static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key) +{ +struct child_process signer =CHILD_PROCESS_INIT ; +int ret =-1 ; +size_t bottom; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *temp =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 (istarts_with (signing_key,"ssh-" )) { +/* A literal ssh key */ + temp =mks_tempfile_t (".git_signing_key_tmpXXXXXX" ); +if (!temp) +return error_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); + goto out; + } + 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" )); + 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 &&strstr (signer_stderr.buf,"usage:" )) { +error (_ ("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)" )); + goto out; + } + +if (ret) { +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,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" )); + goto out; + } + +/* 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); +return ret; +}
Signature · 1 line
gitgitgadget
PATCH v4 3/9 ssh signing: retrieve a default key from ssh-agent
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
if user.signingkey is not set and a ssh signature is requested we call ssh-add -L and use the first key we get
--- gpg-interface.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-)
gpg-interface.c +25 -1
@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +/* 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 "" ; +} +const char *get_signing_key (void ) {if (configured_signing_key)return configured_signing_key; -return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); +if (!strcmp (use_format->name,"ssh" )) { +return get_default_ssh_signing_key (); + }else { +return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); + } }int sign_buffer (struct strbuf *buffer,struct strbuf *signature,const char *signing_key)
Signature · 1 line
gitgitgadget
PATCH v4 4/9 ssh signing: provide a textual representation of the signing key
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ gpg-interface.h | 6 ++++++ send-pack.c | 8 ++++---- 3 files changed, 56 insertions(+), 4 deletions(-)
gpg-interface.c +46 -0
@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +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; + +/* +* With 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 ]) +die_errno (_ ("failed to get the ssh fingerprint for key '%s'" ), + signing_key); + +return strbuf_detach (fingerprint[1 ],NULL ); +} +/* Returns the first public key from an ssh-agent to use for signing */ static char *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 */ +const char *get_signing_key_id (void ) +{ +if (!strcmp (use_format->name,"ssh" )) { +return get_ssh_key_fingerprint (get_signing_key ()); + }else { +/* GPG/GPGSM only store a key id on this variable */ +return get_signing_key (); + } +} +const char *get_signing_key (void ) {if (configured_signing_key)
gpg-interface.h +6 -0
@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); + +/* +* Returns a textual unique representation of the signing key in use +* Either a GPG KeyID or a SSH Key Fingerprint +*/ +const char *get_signing_key_id (void );int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc);
send-pack.c +4 -4
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf, {const struct ref *ref;struct string_list_item *item; -char *signing_key =xstrdup (get_signing_key ()); +char *signing_key_id =xstrdup (get_signing_key_id ());const char *cp, *np;struct strbuf cert =STRBUF_INIT ;int update_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) goto free_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);return update_seen; }
Signature · 1 line
gitgitgadget
PATCH v4 5/9 ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- builtin/receive-pack.c | 2 + gpg-interface.c | 174 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 1 deletion(-)
builtin/receive-pack.c +2 -0
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) {int status =parse_hide_refs_config (var, value,"receive" ); +git_gpg_config (var, value,NULL ); +if (status)return status;
gpg-interface.c +173 -1
209 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v4 6/9 ssh signing: add test prereqs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
generate some ssh keys and a allowedSignersFile for testing
--- t/lib-gpg.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)
t/lib-gpg.sh +27 -0
@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 ' echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null ' +test_lazy_prereq GPGSSH ' + ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1) + test $? != 127 || exit 1 + echo $ssh_version | grep -q "find-principals:missing signature file" + 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" && + cat "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" && + ssh-keygen -t ed25519 -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" + sanitize_pgp() { perl -ne ' /^-----END PGP/ and $in_pgp = 0;
Signature · 1 line
gitgitgadget
PATCH v4 7/9 ssh signing: duplicate t7510 tests for commits
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100755 t/t7527-signed-commit-ssh.sh
t/t7527-signed-commit-ssh.sh new file +398 -0
@@ -0,0 +1,398 @@ +#!/bin/sh + +test_description='ssh signed commit tests' +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh +GNUPGHOME_NOT_USED=$GNUPGHOME +. "$TEST_DIRECTORY/lib-gpg.sh" + +test_expect_success GPGSSH 'create signed commits' ' + test_oid_cache <<-\EOF && + header sha1:gpgsig + header sha256:gpgsig-sha256 + EOF + + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + + echo 1 >file && git add file && + test_tick && git commit -S -m initial && + git tag initial && + git branch side && + + echo 2 >file && test_tick && git commit -a -S -m second && + git tag second && + + git checkout side && + echo 3 >elif && git add elif && + test_tick && git commit -m "third on side" && + + git checkout main && + test_tick && git merge -S side && + git tag merge && + + echo 4 >file && test_tick && git commit -a -m "fourth unsigned" && + git tag fourth-unsigned && + + test_tick && git commit --amend -S -m "fourth signed" && + git tag fourth-signed && + + git config commit.gpgsign true && + echo 5 >file && test_tick && git commit -a -m "fifth signed" && + git tag fifth-signed && + + git config commit.gpgsign false && + echo 6 >file && test_tick && git commit -a -m "sixth" && + git tag sixth-unsigned && + + git config commit.gpgsign true && + echo 7 >file && test_tick && git commit -a -m "seventh" --no-gpg-sign && + git tag seventh-unsigned && + + test_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ && + git tag seventh-signed && + + echo 8 >file && test_tick && git commit -a -m eighth -S"${SIGNING_KEY_UNTRUSTED}" && + git tag eighth-signed-alt && + + # commit.gpgsign is still on but this must not be signed + echo 9 | git commit-tree HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag ninth-unsigned $(cat oid) && + # explicit -S of course must sign. + echo 10 | git commit-tree -S HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag tenth-signed $(cat oid) && + + # --gpg-sign[=<key-id>] must sign. + echo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag eleventh-signed $(cat oid) && + echo 12 | git commit-tree --gpg-sign="${SIGNING_KEY_UNTRUSTED}" HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag twelfth-signed-alt $(cat oid) +' + +test_expect_success GPGSSH 'verify and show signatures' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.mintrustlevel UNDEFINED && + ( + for commit in initial second merge fourth-signed \ + fifth-signed sixth-signed seventh-signed tenth-signed \ + eleventh-signed + do + git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned \ + seventh-unsigned ninth-unsigned + do + test_must_fail git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt twelfth-signed-alt + do + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git verify-commit eighth-signed-alt 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual +' + +test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.minTrustLevel fully && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.minTrustLevel marginal && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' ' + test_config gpg.minTrustLevel ultimate && + test_must_fail git verify-commit eighth-signed-alt +' + +test_expect_success GPGSSH 'verify signatures with --raw' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + ( + for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned + do + test_must_fail git verify-commit --raw $commit 2>actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'proper header is used for hash algorithm' ' + git cat-file commit fourth-signed >output && + grep "^$(test_oid header) -----BEGIN SSH SIGNATURE-----" output +' + +test_expect_success GPGSSH 'show signed commit with signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git show -s initial >commit && + git show -s --show-signature initial >show && + git verify-commit -v initial >verify.1 2>verify.2 && + git cat-file commit initial >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_cmp show.commit commit && + test_cmp show.gpg verify.2 && + test_cmp cat.commit verify.1 +' + +test_expect_success GPGSSH 'detect fudged signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git cat-file commit seventh-signed >raw && + sed -e "s/^seventh/7th forged/" raw >forged1 && + git hash-object -w -t commit forged1 >forged1.commit && + test_must_fail git verify-commit $(cat forged1.commit) && + git show --pretty=short --show-signature $(cat forged1.commit) >actual1 && + grep "${BAD_SIGNATURE}" actual1 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual1 && + ! grep "${GOOD_SIGNATURE_UNTRUSTED}" actual1 +' + +test_expect_success GPGSSH 'detect fudged signature with NUL' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git cat-file commit seventh-signed >raw && + cat raw >forged2 && + echo Qwik | tr "Q" "\000" >>forged2 && + git hash-object -w -t commit forged2 >forged2.commit && + test_must_fail git verify-commit $(cat forged2.commit) && + git show --pretty=short --show-signature $(cat forged2.commit) >actual2 && + grep "${BAD_SIGNATURE}" actual2 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual2 +' + +test_expect_success GPGSSH 'amending already signed commit' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git checkout fourth-signed^0 && + git commit --amend -S --no-edit && + git verify-commit HEAD && + git show -s --show-signature HEAD >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH 'show good signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") && + cat >expect.tmpl <<-\EOF && + G + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect && + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show bad signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect <<-\EOF && + B + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat forged1.commit) >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + U + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with undefined trust level' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + undefined + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + fully + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show lack of signature with custom format' ' + cat >expect <<-\EOF && + N + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" seventh-unsigned >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config log.showsignature true && + git show initial >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH 'check config gpg.format values' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.format ssh && + git commit -S --amend -m "success" && + test_config gpg.format OpEnPgP && + test_must_fail git commit -S --amend -m "fail" +' + +test_expect_failure GPGSSH '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_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig && + gpg -o double-sig2.sig -u 29472784 --detach-sign double-base && + cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc && + sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \ + double-combined.asc > double-gpgsig && + sed -e "/committer/r double-gpgsig" double-base >double-commit && + git hash-object -w -t commit double-commit >double-commit.commit && + test_must_fail git verify-commit $(cat double-commit.commit) && + git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual && + grep "BAD signature from" double-actual && + grep "Good signature from" double-actual +' + +test_expect_failure GPGSSH 'show double signature with custom format (TODO)' ' + cat >expect <<-\EOF && + E + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual && + test_cmp expect actual +' + + +test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' ' + git init multiply-signed && + cd multiply-signed && + test_commit first && + echo 1 >second && + git add second && + tree=$(git write-tree) && + parent=$(git rev-parse HEAD^{commit}) && + git commit --gpg-sign -m second && + git cat-file commit HEAD && + # Avoid trailing whitespace. + sed -e "s/^Q//" -e "s/^Z/ /" >commit <<-EOF && + Qtree $tree + Qparent $parent + Qauthor A U Thor <[email protected]> 1112912653 -0700 + Qcommitter C O Mitter <[email protected]> 1112912653 -0700 + Qgpgsig -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC + Q AKCTqBh+VabJceXcGIZuF0Ry+udbBQ== + Q =tQ0N + Q -----END PGP SIGNATURE----- + Qgpgsig-sha256 -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO + Q AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw== + Q =pIwP + Q -----END PGP SIGNATURE----- + Q + Qsecond + EOF + head=$(git hash-object -t commit -w commit) && + git reset --hard $head && + git verify-commit $head 2>actual && + grep "Good signature from" actual && + ! grep "BAD signature from" actual +' + +test_done
Signature · 1 line
gitgitgadget
PATCH v4 8/9 ssh signing: add more tests for logs, tags & push certs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t4202-log.sh | 23 +++++ t/t5534-push-signed.sh | 101 +++++++++++++++++++ t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100755 t/t7031-verify-tag-signed-ssh.sh
t/t4202-log.sh +23 -0
@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' ' git commit -S -m signed_commit ' +test_expect_success GPGSSH 'setup sshkey signed branch' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_when_finished "git reset --hard && git checkout main" && + git checkout -b signed-ssh main && + echo foo >foo && + git add foo && + git commit -S -m signed_commit +' + test_expect_success GPGSM 'log x509 fingerprint' ' echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && git log -n1 --format="%GF | %GP" signed-x509 >actual &&
@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' ' test_cmp expect actual ' +test_expect_success GPGSSH 'log ssh key fingerprint' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect && + git log -n1 --format="%GF | %GP" signed-ssh >actual && + test_cmp expect actual +' + test_expect_success GPG 'log --graph --show-signature' ' git log --graph --show-signature -n1 signed >actual && grep "^| gpg: Signature made" actual &&
@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' ' grep "^| gpgsm: Good signature" actual ' +test_expect_success GPGSSH 'log --graph --show-signature ssh' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 signed-ssh >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout main" && git checkout -b plain main &&
t/t5534-push-signed.sh +101 -0
113 lines not on this page · raw message
t/t7031-verify-tag-signed-ssh.sh new file +161 -0
161 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v4 9/9 ssh signing: add documentation
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- Documentation/config/gpg.txt | 39 +++++++++++++++++++++++++++++++++-- Documentation/config/user.txt | 6 ++++++ 2 files changed, 43 insertions(+), 2 deletions(-)
Documentation/config/gpg.txt +37 -2
@@ -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.: [email protected],[email protected] 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.
Documentation/config/user.txt +6 -0
@@ -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.
Signature · 1 line
gitgitgadget
Re: [PATCH v4 1/9] ssh signing: preliminary refactoring and clean-up
In reply to Fabian Stelzer via GitGitGadgetQuoted 5 lines · Fabian Stelzer via GitGitGadget
gpg-interface.c +0
-0
gpg-interface.c struct gpg_format { · line 24
Quoted 10 lines
gpg-interface.c +6
-0@@ -15,6 +15,12 @@ struct gpg_format {const char *program;const char **verify_args;const char **sigs; +int (*verify_signed_buffer)(struct signature_check *sigc, +struct gpg_format *fmt,const char *payload, +size_t payload_size,const char *signature, +size_t signature_size); +int (*sign_buffer)(struct strbuf *buffer,struct strbuf *signature, +const char *signing_key); };
Thanks for a pleasant read. Looking good.
Re: [PATCH v4 2/9] ssh signing: add ssh signature format and signing using ssh keys
In reply to Fabian Stelzer via GitGitGadgetgpg-interface.c static struct gpg_format gpg_format[] = { · line 84
Quoted 13 lines · Fabian Stelzer via GitGitGadget
gpg-interface.c +8
-0@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = { .verify_signed_buffer = verify_gpg_signed_buffer, .sign_buffer = sign_buffer_gpg, }, + { + .name = "ssh", + .program = "ssh-keygen", + .verify_args = ssh_verify_args, + .sigs = ssh_sigs, + .verify_signed_buffer = NULL, /* TODO */ + .sign_buffer = sign_buffer_ssh + }, };
A payload a malicious person may feed this version of Git can have a pattern that happens to match the ssh_sigs[] string, and the code will blindly try to call .verify_signed_buffer==NULL and die, no?
That is not the end of the world; as long as we know that with the above "TODO" comment it is probably OK.
gpg-interface.c int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig · line 486
Quoted 6 lines
gpg-interface.c +2
-0@@ -463,12 +482,26 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigreturn use_format->sign_buffer (buffer, signature, signing_key); } +static void strbuf_trim_trailing_cr (struct strbuf *buffer,int offset) +{
This removes any and all CR, not just trimming the trailing ones, so the function is misnamed. Call it remove_cr_after() perhaps?
Alternatively we could tighten the implementation and strip only the CR that come immediately before a LF. That would be a better longer term thing to do, but because you are lifting an existing code from the end of the gpg side of the thing, it may make sense to keep the implementation as-is, but give it a name that is more faithful to what it actually does. When the dust settles, we may want to revisit and fix this helper function to actually trim CRLF into LF (and leave CR in the middle of lines intact), but I do not think it is urgent. Just leaving "NEEDSWORK: make it trim only CRs before LFs and rename" comment would be OK.
Shouldn't the offset (aka bottom) be of type size_t?
I do not recommend giving the function a name that begins with "strbuf_", as it would tempt unthinking person to suggest moving it to strbuf.c, but the presense of the "offset" thing means it will be klunky to reuse in other more generic contexts as a part of the strbuf API.
gpg-interface.c int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig · line 490
Quoted 5 lines
gpg-interface.c +4
-0+size_t i, j; + +for (i = j = offset; i < buffer->len; i++) { +if (buffer->buf[i] !='\r' ) {
Now it gets interesting ;-)
Quoted 6 lines
gpg-interface.c +6
-0+static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key) +{ +struct child_process signer =CHILD_PROCESS_INIT ; +int ret =-1 ; +size_t bottom;
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 550
Quoted 10 lines
gpg-interface.c +10
-0+struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *temp =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 (istarts_with (signing_key,"ssh-" )) {
Is it common in the ssh world to treat ssh- prefix as case insensitive? Not a strong objection but I tend to prefer to start strict unless there is a good reason to be loose when we do not have to, as loosening after the fact is much easier than tightening after starting with a loose definition.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 557
Quoted 7 lines
gpg-interface.c +7
-0+/* A literal ssh key */ + temp =mks_tempfile_t (".git_signing_key_tmpXXXXXX" ); +if (!temp) +return error_errno ( +_ ("could not create temporary file" )); + if (write_in_full(temp->fd, signing_key, strlen(signing_key)) < + 0 ||
"keylen = strlen(signing_key)" before that line, for example, could have easily avoided the line-wrapping at such a place. Wrapping at places like after ||, i.e. after an operator with a low precedence, would make the code easier to follow.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 563
Quoted 6 lines
gpg-interface.c +6
-0+ close_tempfile_gently(temp) < 0) { +error_errno (_ ("failed writing ssh signing key to '%s'" ), + temp->filename.buf); + goto out; + } + ssh_signing_key_file = temp->filename.buf;
It is kind'a sad that we need a fresh temporary file every time, but we can easily tell the user in the documentation that they can use a file with a key in it to avoid it, so it's OK (actually, better than OK, as without this, we may not consume temporary files but we won't offer an ability to take a literal key string).
Is ".git_whatever file in the current directory" a good place to have this temporary file? I would have expected that we would use either $GIT_DIR, $HOME, or $TMPDIR for a thing like this (with different pros-and-cons discussion). At least it is consistent with how a temporary file for the payload to be sign-verified is created, so let's leave it as-is.
Quoted 10 lines
gpg-interface.c +10
-0+ }else { +/* We assume a file */ + ssh_signing_key_file =expand_user_path (signing_key,1 ); + } + + buffer_file =mks_tempfile_t (".git_signing_buffer_tmpXXXXXX" ); +if (!buffer_file) { +error_errno (_ ("could not create temporary file" )); + goto out; + }
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 583
Quoted 10 lines
gpg-interface.c +10
-0+ +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,
Wrap the line before "-n" to keep "-n" and "git" together, if "git" is meant as an argument to the "-n" option.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 592
Quoted 9 lines
gpg-interface.c +9
-0+ 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+)" )); + goto out;
This error message is important to give to the end users, but is it enough? That is, unless "usage:" does not appear, we show the whole raw error message and that would help end users and those helping them to diagnose the issue, but once the underlying program says "usage:", no matter what else it says, it is hidden by this code, since we assume it is a wrong version of openssh.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 598
Quoted 6 lines
gpg-interface.c +6
-0+ } + +if (ret) { +error ("%s" , signer_stderr.buf); + goto out; + }
Also, prehaps
if (ret) {
if (strstr(..., "usage"))
error(_("ssh-keygen -Y sign is needed..."));
else
error("%s", signer_stderr.buf);
goto out;
}would be easier to follow.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 604
Quoted 5 lines
gpg-interface.c +5
-0+ 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 ) {
Is it likely that signature file is smaller than 2kB? I am just wondering how much we care to pick the default that is specific to this codepath vs passing 0 to ask the strbuf API to use whatever default it wants to.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 609
Quoted 5 lines
gpg-interface.c +5
-0+error_errno ( +_ ("failed reading ssh signing data buffer from '%s'" ), + ssh_signature_filename.buf); + } +unlink_or_warn (ssh_signature_filename.buf);
Wait a bit. Even when pipe_command() tells us that we failed, we read from ssh_signature_filename anyway? What is going on? And ...
Review
Quoted 1 line
(quoted diff) +1
-0+ if (ret) {
... does this ever trigger? I thought we would have hit one of the two "goto out" when ret signals an error by being non-zero earlier, and since then nobody touched the variable so far.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 623
Quoted 12 lines
gpg-interface.c +12
-0+error (_ ("ssh failed to sign the data" )); + goto out; + } + +/* 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);
It is clear that the latter one was holding the contents of the buffer to be signed, but reminding the readers what "temp" was about would be a good move. Perhaps renaming the variable to "key_file" or something may help?
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 627
Quoted 4 lines
gpg-interface.c +4
-0+strbuf_release (&signer_stderr); +strbuf_release (&ssh_signature_filename); +return ret; +}
Looking good, except for the "when does 'ret' get updated? shouldn't we refrain from reading the resulting buffer when it is set?" question.
Thanks for a pleasant read.
Re: [PATCH v4 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen
In reply to Fabian Stelzer via GitGitGadgetQuoted 2 lines · Fabian Stelzer via GitGitGadget
create mode 100755 t/t7031-verify-tag-signed-ssh.sh create mode 100755 t/t7527-signed-commit-ssh.sh
As the number 7527 is already in use by another topic in 'seen', this new one must be relocated to coexist with them.
Re: [PATCH v4 2/9] ssh signing: add ssh signature format and signing using ssh keys
In reply to Junio C HamanoQuoted 19 lines · Junio C Hamano
gpg-interface.c static struct gpg_format gpg_format[] = { · line 84
Quoted 13 lines · Fabian Stelzer via GitGitGadget
gpg-interface.c +8
-0@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = { .verify_signed_buffer = verify_gpg_signed_buffer, .sign_buffer = sign_buffer_gpg, }, + { + .name = "ssh", + .program = "ssh-keygen", + .verify_args = ssh_verify_args, + .sigs = ssh_sigs, + .verify_signed_buffer = NULL, /* TODO */ + .sign_buffer = sign_buffer_ssh + }, };A payload a malicious person may feed this version of Git can have a pattern that happens to match the ssh_sigs[] string, and the code will blindly try to call .verify_signed_buffer==NULL and die, no?
Quoted 2 lines
That is not the end of the world; as long as we know that with the above "TODO" comment it is probably OK.
I thought about adding an if(!fmt->sign) BUG() but since these callbacks are static it shoud really only be an issue between patches of the set.
Quoted 10 lines
gpg-interface.c int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig · line 486
Quoted 7 lines
gpg-interface.c +2
-0@@ -463,12 +482,26 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigreturn use_format->sign_buffer (buffer, signature, signing_key); } +static void strbuf_trim_trailing_cr (struct strbuf *buffer,int offset) +{This removes any and all CR, not just trimming the trailing ones, so the function is misnamed. Call it
remove_cr_after()perhaps?
Quoted 10 lines
Alternatively we could tighten the implementation and strip only the CR that come immediately before a LF. That would be a better longer term thing to do, but because you are lifting an existing code from the end of the gpg side of the thing, it may make sense to keep the implementation as-is, but give it a name that is more faithful to what it actually does. When the dust settles, we may want to revisit and fix this helper function to actually trim CRLF into LF (and leave CR in the middle of lines intact), but I do not think it is urgent. Just leaving "NEEDSWORK: make it trim only CRs before LFs and rename" comment would be OK.
Agreed. I've renamed it and added the comment.
Quoted 2 lines
Shouldn't the offset (aka bottom) be of type
size_t?
fixed
Quoted 13 lines
Quoted 13 lines
gpg-interface.c +13
-0+static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key) +{ +struct child_process signer =CHILD_PROCESS_INIT ; +int ret =-1 ; +size_t bottom; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *temp =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(
Quoted 8 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 550
Quoted 3 lines
gpg-interface.c +3
-0+ _("user.signingkey needs to be set for ssh signing")); + +if (istarts_with (signing_key,"ssh-" )) {Is it common in the ssh world to treat ssh- prefix as case insensitive? Not a strong objection but I tend to prefer to start strict unless there is a good reason to be loose when we do not have to, as loosening after the fact is much easier than tightening after starting with a loose definition.
I don't think so. I will make it case sensitive.
Quoted 12 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 557
Quoted 8 lines
gpg-interface.c +7
-0+/* A literal ssh key */ + temp =mks_tempfile_t (".git_signing_key_tmpXXXXXX" ); +if (!temp) +return error_errno ( +_ ("could not create temporary file" )); + if (write_in_full(temp->fd, signing_key, strlen(signing_key)) < + 0 ||"keylen =
strlen(signing_key)" before that line, for example, could have easily avoided the line-wrapping at such a place. Wrapping at places like after ||, i.e. after an operator with a low precedence, would make the code easier to follow.
agreed.
Quoted 13 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 563
Quoted 7 lines
gpg-interface.c +6
-0+ close_tempfile_gently(temp) < 0) { +error_errno (_ ("failed writing ssh signing key to '%s'" ), + temp->filename.buf); + goto out; + } + ssh_signing_key_file = temp->filename.buf;It is kind'a sad that we need a fresh temporary file every time, but we can easily tell the user in the documentation that they can use a file with a key in it to avoid it, so it's OK (actually, better than OK, as without this, we may not consume temporary files but we won't offer an ability to take a literal key string).
Quoted 6 lines
Is ".git_whatever file in the current directory" a good place to have this temporary file? I would have expected that we would use either $GIT_DIR, $HOME, or $TMPDIR for a thing like this (with different pros-and-cons discussion). At least it is consistent with how a temporary file for the payload to be sign-verified is created, so let's leave it as-is.
Intuitively i thought just using mks_tempfile_t() would choose a good dir for such files.
Quoted 15 lines
Quoted 15 lines
gpg-interface.c +14
-0+ }else { +/* We assume a file */ + ssh_signing_key_file =expand_user_path (signing_key,1 ); + } + + buffer_file =mks_tempfile_t (".git_signing_buffer_tmpXXXXXX" ); +if (!buffer_file) { +error_errno (_ ("could not create temporary file" )); + goto out; + } + +if (write_in_full (buffer_file->fd, buffer->buf, buffer->len) <0 || +close_tempfile_gently (buffer_file) <0 ) { + error_errno(_("failed writing ssh signing key buffer to '%s'"),
Quoted 8 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 583
Quoted 6 lines
gpg-interface.c +6
-0+ buffer_file->filename.buf); + goto out; + } + + strvec_pushl(&signer.args, use_format->program, "-Y", "sign", "-n", + "git", "-f", ssh_signing_key_file,Wrap the line before "-n" to keep "-n" and "git" together, if "git" is meant as an argument to the "-n" option.
done. some things clang-format can't really understand. overall it is quite helpful but a few things i still had to reformat.
Quoted 33 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 592
Quoted 10 lines
gpg-interface.c +9
-0+ 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+)" )); + goto out;This error message is important to give to the end users, but is it enough? That is, unless "usage:" does not appear, we show the whole raw error message and that would help end users and those helping them to diagnose the issue, but once the underlying program says "usage:", no matter what else it says, it is hidden by this code, since we assume it is a wrong version of openssh.
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 598
Quoted 6 lines
gpg-interface.c +6
-0+ } + +if (ret) { +error ("%s" , signer_stderr.buf); + goto out; + }Also, prehaps
if (ret) { if (strstr(..., "usage")) error(_("ssh-keygen -Y sign is needed...")); else error("%s", signer_stderr.buf); goto out; }
Quoted 1 line
would be easier to follow.
I have changed this to:
if (ret) {
if (strstr(..., "usage")) error(_("ssh-keygen -Y sign is needed..."));
error("%s", signer_stderr.buf);
goto out;
}
and removed the if (ret) further down in the function that had no effect.
I had removed the raw stderr output from verify & sign because it becomes quite unreadable when doing a "git log --show-signature" with ssh signatures present and not support in ssh for it.
I think in case of signing the full output is good. The user is taking an active action (wanting to sign something) so we should give them all the help we can when things go wrong.
The output for verification is debatable. The config might have a "log.showSignature" for verifying gpg signatures and when ssh signatures show up we should tell them it's not supported on their setup but probably not showing endless lines of errors when they do a "git log". A "git verify-commit" might be a different case. But code-path-wise this is the same thing at the moment.
Quoted 8 lines
Quoted 8 lines
gpg-interface.c +8
-0+error (_ ("ssh failed to sign the data" )); + goto out; + } + +/* Strip CR from the line endings, in case we are on Windows. */ +strbuf_trim_trailing_cr (signature, bottom); + +out:
Quoted 8 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 623
Quoted 4 lines
gpg-interface.c +4
-0+if (temp) +delete_tempfile (&temp); +if (buffer_file) +delete_tempfile (&buffer_file);It is clear that the latter one was holding the contents of the buffer to be signed, but reminding the readers what "temp" was about would be a good move. Perhaps renaming the variable to "
key_file" or something may help?
renamed to "key_file"
Quoted 10 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 627
Quoted 5 lines
gpg-interface.c +4
-0+strbuf_release (&signer_stderr); +strbuf_release (&ssh_signature_filename); +return ret; +}Looking good, except for the "when does 'ret' get updated? shouldn't we refrain from reading the resulting buffer when it is set?" question.
Thanks for a pleasant read.
Thanks a lot for your support with this!
Quoted 1 line
v5 cover ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen
In reply to Fabian Stelzer via GitGitGadgetI 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
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 | 27 ++ 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, 1216 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-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v5
Pull-Request: https://github.com/git/git/pull/1041
Range-diff vs v4:
1: b4b0e2bac1c = 1: 7c8502c65b8 ssh signing: preliminary refactoring and clean-up
2: 2c75adee8e1 ! 2: f05bab16096 ssh signing: add ssh signature format and signing using ssh keys
@@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature
return use_format->sign_buffer(buffer, signature, signing_key);
}
-+static void strbuf_trim_trailing_cr(struct strbuf *buffer, int offset)
++/*
++ * 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;
+
@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf
- j++;
- }
- strbuf_setlen(signature, j);
-+ strbuf_trim_trailing_cr(signature, bottom);
++ remove_cr_after(signature, bottom);
return 0;
}
@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf
+{
+ struct child_process signer = CHILD_PROCESS_INIT;
+ int ret = -1;
-+ size_t bottom;
++ size_t bottom, keylen;
+ struct strbuf signer_stderr = STRBUF_INIT;
-+ struct tempfile *temp = NULL, *buffer_file = NULL;
++ struct tempfile *key_file = NULL, *buffer_file = NULL;
+ char *ssh_signing_key_file = NULL;
+ struct strbuf ssh_signature_filename = STRBUF_INIT;
+
@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf
+ return error(
+ _("user.signingkey needs to be set for ssh signing"));
+
-+ if (istarts_with(signing_key, "ssh-")) {
++ if (starts_with(signing_key, "ssh-")) {
+ /* A literal ssh key */
-+ temp = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
-+ if (!temp)
++ key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
++ if (!key_file)
+ return error_errno(
+ _("could not create temporary file"));
-+ if (write_in_full(temp->fd, signing_key, strlen(signing_key)) <
-+ 0 ||
-+ close_tempfile_gently(temp) < 0) {
++ 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'"),
-+ temp->filename.buf);
++ key_file->filename.buf);
+ goto out;
+ }
-+ ssh_signing_key_file = temp->filename.buf;
++ ssh_signing_key_file = key_file->filename.buf;
+ } else {
+ /* We assume a file */
+ ssh_signing_key_file = expand_user_path(signing_key, 1);
@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf
+ goto out;
+ }
+
-+ strvec_pushl(&signer.args, use_format->program, "-Y", "sign", "-n",
-+ "git", "-f", ssh_signing_key_file,
-+ buffer_file->filename.buf, NULL);
++ 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+)"));
-+ goto out;
-+ }
-+
+ 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;
+ }
@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf
+
+ 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) {
++ 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);
+
-+ if (ret) {
-+ error(_("ssh failed to sign the data"));
-+ goto out;
-+ }
-+
+ /* Strip CR from the line endings, in case we are on Windows. */
-+ strbuf_trim_trailing_cr(signature, bottom);
++ remove_cr_after(signature, bottom);
+
+out:
-+ if (temp)
-+ delete_tempfile(&temp);
++ if (key_file)
++ delete_tempfile(&key_file);
+ if (buffer_file)
+ delete_tempfile(&buffer_file);
+ strbuf_release(&signer_stderr);
3: 1ec5c06cbe9 = 3: 071e6173d8e ssh signing: retrieve a default key from ssh-agent
4: ec6931082ee = 4: 7d1d131ff5b ssh signing: provide a textual representation of the signing key
5: 4436cb3a122 ! 5: 725764018ce ssh signing: parse ssh-keygen output and verify signatures
@@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
+ }
+}
+
-+static const char *get_ssh_allowed_signers(void)
-+{
-+ if (ssh_allowed_signers)
-+ return ssh_allowed_signers;
-+
-+ die("gpg.ssh.allowedSignersFile needs to be configured and exist for validation");
-+}
-+
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
+ struct gpg_format *fmt, const char *payload,
+ size_t payload_size, const char *signature,
+ size_t signature_size)
+{
+ struct child_process ssh_keygen = CHILD_PROCESS_INIT;
-+ struct tempfile *temp;
-+ int ret;
++ struct tempfile *buffer_file;
++ int ret = -1;
+ const char *line;
+ size_t trust_size;
+ char *principal;
+ struct strbuf ssh_keygen_out = STRBUF_INIT;
+ struct strbuf ssh_keygen_err = STRBUF_INIT;
+
-+ temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
-+ if (!temp)
++ if (!ssh_allowed_signers) {
++ error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification"));
++ return -1;
++ }
++
++ buffer_file = mks_tempfile_t(".git_vtag_tmpXXXXXX");
++ if (!buffer_file)
+ return error_errno(_("could not create temporary file"));
-+ if (write_in_full(temp->fd, signature, signature_size) < 0 ||
-+ close_tempfile_gently(temp) < 0) {
++ 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'"),
-+ temp->filename.buf);
-+ delete_tempfile(&temp);
++ 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", get_ssh_allowed_signers(), "-s", temp->filename.buf,
++ 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+)"));
-+ return ret;
++ goto out;
+ }
+ if (ret || !ssh_keygen_out.len) {
+ /* We did not find a matching principal in the allowedSigners - Check
+ * without validation */
+ child_process_init(&ssh_keygen);
-+ strvec_pushl(&ssh_keygen.args, fmt->program, "-Y",
-+ "check-novalidate", "-n", "git", "-s",
-+ temp->filename.buf, NULL);
++ 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 {
@@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
+ 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,
++ 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) {
@@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
+
+ parse_ssh_output(sigc);
+
-+ delete_tempfile(&temp);
++out:
++ if (buffer_file)
++ delete_tempfile(&buffer_file);
+ strbuf_release(&ssh_keygen_out);
+ strbuf_release(&ssh_keygen_err);
+
6: 06a76e64b35 = 6: eb677b1b6a8 ssh signing: add test prereqs
7: 4dc5572083b ! 7: c877951df23 ssh signing: duplicate t7510 tests for commits@@ Commit message
- ## t/t7527-signed-commit-ssh.sh (new) ##
+ ## t/t7528-signed-commit-ssh.sh (new) ##
@@
+#!/bin/sh
+
8: 275dd8a1013 = 8: 60265e8c399 ssh signing: add more tests for logs, tags & push certs 9: 13f6c229bd1 = 9: f758ce0ade4 ssh signing: add documentation
Signature · 1 line
gitgitgadget
PATCH v5 1/9 ssh signing: preliminary refactoring and clean-up
From: Fabian Stelzer <[email protected]>
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
--- 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(-)
fmt-merge-msg.c +3 -3
@@ -526,11 +526,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out) buf = payload.buf; len = payload.len;if (check_signature (payload.buf, payload.len, sig.buf, - sig.len, &sigc) && - !sigc.gpg_output) + sig.len, &sigc) && + !sigc.output)strbuf_addstr (&sig,"gpg verification failed.\n" );else -strbuf_addstr (&sig, sigc.gpg_output); +strbuf_addstr (&sig, sigc.output); }signature_check_clear (&sigc);
gpg-interface.c +64 -40
176 lines not on this page · raw message
gpg-interface.h +1 -1
@@ -17,7 +17,7 @@ enum signature_trust_level {struct signature_check {char *payload; -char *gpg_output; +char *output;char *gpg_status;/*
log-tree.c +4 -4
@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit) status =check_signature (payload.buf, payload.len, signature.buf, signature.len, &sigc); -if (status && !sigc.gpg_output) +if (status && !sigc.output)show_sig_lines (opt, status,"No signature\n" );else -show_sig_lines (opt, status, sigc.gpg_output); +show_sig_lines (opt, status, sigc.output);signature_check_clear (&sigc); out:
@@ -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);else strbuf_addstr (&verify_message,"No signature\n" );signature_check_clear (&sigc);
pretty.c +2 -2
@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */check_commit_signature (c->commit, &(c->signature_check));switch (placeholder[1 ]) {case 'G' : -if (c->signature_check.gpg_output) -strbuf_addstr (sb, c->signature_check.gpg_output); +if (c->signature_check.output) +strbuf_addstr (sb, c->signature_check.output);break ;case '?' :switch (c->signature_check.result) {
Signature · 1 line
gitgitgadget
PATCH v5 2/9 ssh signing: add ssh signature format and signing using ssh keys
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 8 deletions(-)
gpg-interface.c +129 -8
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {NULL }; +static const char *ssh_verify_args[] = {NULL }; +static const char *ssh_sigs[] = { +"-----BEGIN SSH SIGNATURE-----" , +NULL +}; +static int verify_gpg_signed_buffer (struct signature_check *sigc,struct gpg_format *fmt,const char *payload,size_t payload_size,const char *signature,size_t signature_size);static int sign_buffer_gpg (struct strbuf *buffer,struct strbuf *signature,const char *signing_key); +static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key);static struct gpg_format gpg_format[] = { {
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = { .verify_signed_buffer = verify_gpg_signed_buffer, .sign_buffer = sign_buffer_gpg, }, + { + .name = "ssh", + .program = "ssh-keygen", + .verify_args = ssh_verify_args, + .sigs = ssh_sigs, + .verify_signed_buffer = NULL, /* TODO */ + .sign_buffer = sign_buffer_ssh + }, };static struct gpg_format *use_format = &gpg_format[0 ];
@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)if (!strcmp (var,"gpg.x509.program" )) fmtname ="x509" ; +if (!strcmp (var,"gpg.ssh.program" )) + fmtname ="ssh" ; +if (fmtname) { fmt =get_format_by_name (fmtname);return git_config_string (&fmt->program, var, value);
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigreturn use_format->sign_buffer (buffer, signature, signing_key); } +/* +* Strip CR from the line endings, in case we are on Windows. +* NEEDSWORK: make it trim only CRs before LFs and rename +*/ +static void remove_cr_after (struct strbuf *buffer,size_t offset) +{ +size_t i, j; + +for (i = j = offset; i < buffer->len; i++) { +if (buffer->buf[i] !='\r' ) { +if (i != j) + buffer->buf[j] = buffer->buf[i]; + j++; + } + } +strbuf_setlen (buffer, j); +} +static int sign_buffer_gpg (struct strbuf *buffer,struct strbuf *signature,const char *signing_key) {struct child_process gpg =CHILD_PROCESS_INIT ;int ret; -size_t i, j, bottom; +size_t bottom;struct strbuf gpg_status =STRBUF_INIT ; strvec_pushl(&gpg.args,
@@ -494,13 +531,97 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,return error (_ ("gpg failed to sign the data" ));/* Strip CR from the line endings, in case we are on Windows. */ -for (i = j = bottom; i < signature->len; i++) -if (signature->buf[i] !='\r' ) { -if (i != j) - signature->buf[j] = signature->buf[i]; - j++; - } -strbuf_setlen (signature, j); +remove_cr_after (signature, bottom);return 0 ; } + +static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key) +{ +struct child_process signer =CHILD_PROCESS_INIT ; +int ret =-1 ; +size_t bottom, keylen; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *key_file =NULL , *buffer_file =NULL ; +char *ssh_signing_key_file =NULL ; +struct strbuf ssh_signature_filename =STRBUF_INIT ; + +if (!signing_key || signing_key[0 ] =='\0' ) +return error ( +_ ("user.signingkey needs to be set for ssh signing" )); + +if (starts_with (signing_key,"ssh-" )) { +/* A literal ssh key */ + key_file =mks_tempfile_t (".git_signing_key_tmpXXXXXX" ); +if (!key_file) +return error_errno ( +_ ("could not create temporary file" )); + keylen =strlen (signing_key); +if (write_in_full (key_file->fd, signing_key, keylen) <0 || +close_tempfile_gently (key_file) <0 ) { +error_errno (_ ("failed writing ssh signing key to '%s'" ), + key_file->filename.buf); + goto out; + } + ssh_signing_key_file = 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" )); + goto out; + } + +if (write_in_full (buffer_file->fd, buffer->buf, buffer->len) <0 || +close_tempfile_gently (buffer_file) <0 ) { +error_errno (_ ("failed writing ssh signing key buffer to '%s'" ), + buffer_file->filename.buf); + goto out; + } + +strvec_pushl (&signer.args, use_format->program, +"-Y" ,"sign" , +"-n" ,"git" , +"-f" , ssh_signing_key_file, + buffer_file->filename.buf, +NULL ); + +sigchain_push (SIGPIPE ,SIG_IGN ); + ret =pipe_command (&signer,NULL ,0 ,NULL ,0 , &signer_stderr,0 ); +sigchain_pop (SIGPIPE ); + +if (ret) { +if (strstr (signer_stderr.buf,"usage:" )) +error (_ ("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)" )); + +error ("%s" , signer_stderr.buf); + goto out; + } + + bottom = signature->len; + +strbuf_addbuf (&ssh_signature_filename, &buffer_file->filename); +strbuf_addstr (&ssh_signature_filename,".sig" ); +if (strbuf_read_file (signature, ssh_signature_filename.buf,0 ) <0 ) { +error_errno ( +_ ("failed reading ssh signing data buffer from '%s'" ), + ssh_signature_filename.buf); + } +unlink_or_warn (ssh_signature_filename.buf); + +/* Strip CR from the line endings, in case we are on Windows. */ +remove_cr_after (signature, bottom); + +out: +if (key_file) +delete_tempfile (&key_file); +if (buffer_file) +delete_tempfile (&buffer_file); +strbuf_release (&signer_stderr); +strbuf_release (&ssh_signature_filename); +return ret; +}
Signature · 1 line
gitgitgadget
PATCH v5 4/9 ssh signing: provide a textual representation of the signing key
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ gpg-interface.h | 6 ++++++ send-pack.c | 8 ++++---- 3 files changed, 56 insertions(+), 4 deletions(-)
gpg-interface.c +46 -0
@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +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; + +/* +* With 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 ]) +die_errno (_ ("failed to get the ssh fingerprint for key '%s'" ), + signing_key); + +return strbuf_detach (fingerprint[1 ],NULL ); +} +/* Returns the first public key from an ssh-agent to use for signing */ static char *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 */ +const char *get_signing_key_id (void ) +{ +if (!strcmp (use_format->name,"ssh" )) { +return get_ssh_key_fingerprint (get_signing_key ()); + }else { +/* GPG/GPGSM only store a key id on this variable */ +return get_signing_key (); + } +} +const char *get_signing_key (void ) {if (configured_signing_key)
gpg-interface.h +6 -0
@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); + +/* +* Returns a textual unique representation of the signing key in use +* Either a GPG KeyID or a SSH Key Fingerprint +*/ +const char *get_signing_key_id (void );int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc);
send-pack.c +4 -4
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf, {const struct ref *ref;struct string_list_item *item; -char *signing_key =xstrdup (get_signing_key ()); +char *signing_key_id =xstrdup (get_signing_key_id ());const char *cp, *np;struct strbuf cert =STRBUF_INIT ;int update_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) goto free_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);return update_seen; }
Signature · 1 line
gitgitgadget
PATCH v5 3/9 ssh signing: retrieve a default key from ssh-agent
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
if user.signingkey is not set and a ssh signature is requested we call ssh-add -L and use the first key we get
--- gpg-interface.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-)
gpg-interface.c +25 -1
@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +/* 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 "" ; +} +const char *get_signing_key (void ) {if (configured_signing_key)return configured_signing_key; -return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); +if (!strcmp (use_format->name,"ssh" )) { +return get_default_ssh_signing_key (); + }else { +return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); + } }int sign_buffer (struct strbuf *buffer,struct strbuf *signature,const char *signing_key)
Signature · 1 line
gitgitgadget
PATCH v5 5/9 ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- builtin/receive-pack.c | 2 + gpg-interface.c | 179 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 1 deletion(-)
builtin/receive-pack.c +2 -0
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) {int status =parse_hide_refs_config (var, value,"receive" ); +git_gpg_config (var, value,NULL ); +if (status)return status;
gpg-interface.c +178 -1
214 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v5 6/9 ssh signing: add test prereqs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
generate some ssh keys and a allowedSignersFile for testing
--- t/lib-gpg.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)
t/lib-gpg.sh +27 -0
@@ -87,6 +87,33 @@ test_lazy_prereq RFC1991 ' echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null ' +test_lazy_prereq GPGSSH ' + ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1) + test $? != 127 || exit 1 + echo $ssh_version | grep -q "find-principals:missing signature file" + 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" && + cat "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" && + ssh-keygen -t ed25519 -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" + sanitize_pgp() { perl -ne ' /^-----END PGP/ and $in_pgp = 0;
Signature · 1 line
gitgitgadget
PATCH v5 7/9 ssh signing: duplicate t7510 tests for commits
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100755 t/t7528-signed-commit-ssh.sh
t/t7528-signed-commit-ssh.sh new file +398 -0
@@ -0,0 +1,398 @@ +#!/bin/sh + +test_description='ssh signed commit tests' +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh +GNUPGHOME_NOT_USED=$GNUPGHOME +. "$TEST_DIRECTORY/lib-gpg.sh" + +test_expect_success GPGSSH 'create signed commits' ' + test_oid_cache <<-\EOF && + header sha1:gpgsig + header sha256:gpgsig-sha256 + EOF + + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + + echo 1 >file && git add file && + test_tick && git commit -S -m initial && + git tag initial && + git branch side && + + echo 2 >file && test_tick && git commit -a -S -m second && + git tag second && + + git checkout side && + echo 3 >elif && git add elif && + test_tick && git commit -m "third on side" && + + git checkout main && + test_tick && git merge -S side && + git tag merge && + + echo 4 >file && test_tick && git commit -a -m "fourth unsigned" && + git tag fourth-unsigned && + + test_tick && git commit --amend -S -m "fourth signed" && + git tag fourth-signed && + + git config commit.gpgsign true && + echo 5 >file && test_tick && git commit -a -m "fifth signed" && + git tag fifth-signed && + + git config commit.gpgsign false && + echo 6 >file && test_tick && git commit -a -m "sixth" && + git tag sixth-unsigned && + + git config commit.gpgsign true && + echo 7 >file && test_tick && git commit -a -m "seventh" --no-gpg-sign && + git tag seventh-unsigned && + + test_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ && + git tag seventh-signed && + + echo 8 >file && test_tick && git commit -a -m eighth -S"${SIGNING_KEY_UNTRUSTED}" && + git tag eighth-signed-alt && + + # commit.gpgsign is still on but this must not be signed + echo 9 | git commit-tree HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag ninth-unsigned $(cat oid) && + # explicit -S of course must sign. + echo 10 | git commit-tree -S HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag tenth-signed $(cat oid) && + + # --gpg-sign[=<key-id>] must sign. + echo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag eleventh-signed $(cat oid) && + echo 12 | git commit-tree --gpg-sign="${SIGNING_KEY_UNTRUSTED}" HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag twelfth-signed-alt $(cat oid) +' + +test_expect_success GPGSSH 'verify and show signatures' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.mintrustlevel UNDEFINED && + ( + for commit in initial second merge fourth-signed \ + fifth-signed sixth-signed seventh-signed tenth-signed \ + eleventh-signed + do + git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned \ + seventh-unsigned ninth-unsigned + do + test_must_fail git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt twelfth-signed-alt + do + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git verify-commit eighth-signed-alt 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual +' + +test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.minTrustLevel fully && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.minTrustLevel marginal && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' ' + test_config gpg.minTrustLevel ultimate && + test_must_fail git verify-commit eighth-signed-alt +' + +test_expect_success GPGSSH 'verify signatures with --raw' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + ( + for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned + do + test_must_fail git verify-commit --raw $commit 2>actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'proper header is used for hash algorithm' ' + git cat-file commit fourth-signed >output && + grep "^$(test_oid header) -----BEGIN SSH SIGNATURE-----" output +' + +test_expect_success GPGSSH 'show signed commit with signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git show -s initial >commit && + git show -s --show-signature initial >show && + git verify-commit -v initial >verify.1 2>verify.2 && + git cat-file commit initial >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_cmp show.commit commit && + test_cmp show.gpg verify.2 && + test_cmp cat.commit verify.1 +' + +test_expect_success GPGSSH 'detect fudged signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git cat-file commit seventh-signed >raw && + sed -e "s/^seventh/7th forged/" raw >forged1 && + git hash-object -w -t commit forged1 >forged1.commit && + test_must_fail git verify-commit $(cat forged1.commit) && + git show --pretty=short --show-signature $(cat forged1.commit) >actual1 && + grep "${BAD_SIGNATURE}" actual1 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual1 && + ! grep "${GOOD_SIGNATURE_UNTRUSTED}" actual1 +' + +test_expect_success GPGSSH 'detect fudged signature with NUL' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git cat-file commit seventh-signed >raw && + cat raw >forged2 && + echo Qwik | tr "Q" "\000" >>forged2 && + git hash-object -w -t commit forged2 >forged2.commit && + test_must_fail git verify-commit $(cat forged2.commit) && + git show --pretty=short --show-signature $(cat forged2.commit) >actual2 && + grep "${BAD_SIGNATURE}" actual2 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual2 +' + +test_expect_success GPGSSH 'amending already signed commit' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git checkout fourth-signed^0 && + git commit --amend -S --no-edit && + git verify-commit HEAD && + git show -s --show-signature HEAD >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH 'show good signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") && + cat >expect.tmpl <<-\EOF && + G + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect && + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show bad signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect <<-\EOF && + B + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat forged1.commit) >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + U + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with undefined trust level' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + undefined + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + fully + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show lack of signature with custom format' ' + cat >expect <<-\EOF && + N + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" seventh-unsigned >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config log.showsignature true && + git show initial >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH 'check config gpg.format values' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.format ssh && + git commit -S --amend -m "success" && + test_config gpg.format OpEnPgP && + test_must_fail git commit -S --amend -m "fail" +' + +test_expect_failure GPGSSH '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_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig && + gpg -o double-sig2.sig -u 29472784 --detach-sign double-base && + cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc && + sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \ + double-combined.asc > double-gpgsig && + sed -e "/committer/r double-gpgsig" double-base >double-commit && + git hash-object -w -t commit double-commit >double-commit.commit && + test_must_fail git verify-commit $(cat double-commit.commit) && + git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual && + grep "BAD signature from" double-actual && + grep "Good signature from" double-actual +' + +test_expect_failure GPGSSH 'show double signature with custom format (TODO)' ' + cat >expect <<-\EOF && + E + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual && + test_cmp expect actual +' + + +test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' ' + git init multiply-signed && + cd multiply-signed && + test_commit first && + echo 1 >second && + git add second && + tree=$(git write-tree) && + parent=$(git rev-parse HEAD^{commit}) && + git commit --gpg-sign -m second && + git cat-file commit HEAD && + # Avoid trailing whitespace. + sed -e "s/^Q//" -e "s/^Z/ /" >commit <<-EOF && + Qtree $tree + Qparent $parent + Qauthor A U Thor <[email protected]> 1112912653 -0700 + Qcommitter C O Mitter <[email protected]> 1112912653 -0700 + Qgpgsig -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC + Q AKCTqBh+VabJceXcGIZuF0Ry+udbBQ== + Q =tQ0N + Q -----END PGP SIGNATURE----- + Qgpgsig-sha256 -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO + Q AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw== + Q =pIwP + Q -----END PGP SIGNATURE----- + Q + Qsecond + EOF + head=$(git hash-object -t commit -w commit) && + git reset --hard $head && + git verify-commit $head 2>actual && + grep "Good signature from" actual && + ! grep "BAD signature from" actual +' + +test_done
Signature · 1 line
gitgitgadget
PATCH v5 8/9 ssh signing: add more tests for logs, tags & push certs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t4202-log.sh | 23 +++++ t/t5534-push-signed.sh | 101 +++++++++++++++++++ t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100755 t/t7031-verify-tag-signed-ssh.sh
t/t4202-log.sh +23 -0
@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' ' git commit -S -m signed_commit ' +test_expect_success GPGSSH 'setup sshkey signed branch' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_when_finished "git reset --hard && git checkout main" && + git checkout -b signed-ssh main && + echo foo >foo && + git add foo && + git commit -S -m signed_commit +' + test_expect_success GPGSM 'log x509 fingerprint' ' echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && git log -n1 --format="%GF | %GP" signed-x509 >actual &&
@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' ' test_cmp expect actual ' +test_expect_success GPGSSH 'log ssh key fingerprint' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect && + git log -n1 --format="%GF | %GP" signed-ssh >actual && + test_cmp expect actual +' + test_expect_success GPG 'log --graph --show-signature' ' git log --graph --show-signature -n1 signed >actual && grep "^| gpg: Signature made" actual &&
@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' ' grep "^| gpgsm: Good signature" actual ' +test_expect_success GPGSSH 'log --graph --show-signature ssh' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 signed-ssh >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout main" && git checkout -b plain main &&
t/t5534-push-signed.sh +101 -0
113 lines not on this page · raw message
t/t7031-verify-tag-signed-ssh.sh new file +161 -0
161 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v5 9/9 ssh signing: add documentation
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- Documentation/config/gpg.txt | 39 +++++++++++++++++++++++++++++++++-- Documentation/config/user.txt | 6 ++++++ 2 files changed, 43 insertions(+), 2 deletions(-)
Documentation/config/gpg.txt +37 -2
@@ -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.: [email protected],[email protected] 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.
Documentation/config/user.txt +6 -0
@@ -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.
Signature · 1 line
gitgitgadget
v6 cover ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen
In reply to Fabian Stelzer via GitGitGadgetI 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 documentationSignature · 1 line
gitgitgadget
PATCH v6 1/9 ssh signing: preliminary refactoring and clean-up
From: Fabian Stelzer <[email protected]>
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
--- 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(-)
fmt-merge-msg.c +3 -3
@@ -526,11 +526,11 @@ static void fmt_merge_msg_sigs(struct strbuf *out) buf = payload.buf; len = payload.len;if (check_signature (payload.buf, payload.len, sig.buf, - sig.len, &sigc) && - !sigc.gpg_output) + sig.len, &sigc) && + !sigc.output)strbuf_addstr (&sig,"gpg verification failed.\n" );else -strbuf_addstr (&sig, sigc.gpg_output); +strbuf_addstr (&sig, sigc.output); }signature_check_clear (&sigc);
gpg-interface.c +64 -40
176 lines not on this page · raw message
gpg-interface.h +1 -1
@@ -17,7 +17,7 @@ enum signature_trust_level {struct signature_check {char *payload; -char *gpg_output; +char *output;char *gpg_status;/*
log-tree.c +4 -4
@@ -513,10 +513,10 @@ static void show_signature(struct rev_info *opt, struct commit *commit) status =check_signature (payload.buf, payload.len, signature.buf, signature.len, &sigc); -if (status && !sigc.gpg_output) +if (status && !sigc.output)show_sig_lines (opt, status,"No signature\n" );else -show_sig_lines (opt, status, sigc.gpg_output); +show_sig_lines (opt, status, sigc.output);signature_check_clear (&sigc); out:
@@ -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);else strbuf_addstr (&verify_message,"No signature\n" );signature_check_clear (&sigc);
pretty.c +2 -2
@@ -1432,8 +1432,8 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */check_commit_signature (c->commit, &(c->signature_check));switch (placeholder[1 ]) {case 'G' : -if (c->signature_check.gpg_output) -strbuf_addstr (sb, c->signature_check.gpg_output); +if (c->signature_check.output) +strbuf_addstr (sb, c->signature_check.output);break ;case '?' :switch (c->signature_check.result) {
Signature · 1 line
gitgitgadget
PATCH v6 2/9 ssh signing: add ssh signature format and signing using ssh keys
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 8 deletions(-)
gpg-interface.c +129 -8
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {NULL }; +static const char *ssh_verify_args[] = {NULL }; +static const char *ssh_sigs[] = { +"-----BEGIN SSH SIGNATURE-----" , +NULL +}; +static int verify_gpg_signed_buffer (struct signature_check *sigc,struct gpg_format *fmt,const char *payload,size_t payload_size,const char *signature,size_t signature_size);static int sign_buffer_gpg (struct strbuf *buffer,struct strbuf *signature,const char *signing_key); +static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key);static struct gpg_format gpg_format[] = { {
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = { .verify_signed_buffer = verify_gpg_signed_buffer, .sign_buffer = sign_buffer_gpg, }, + { + .name = "ssh", + .program = "ssh-keygen", + .verify_args = ssh_verify_args, + .sigs = ssh_sigs, + .verify_signed_buffer = NULL, /* TODO */ + .sign_buffer = sign_buffer_ssh + }, };static struct gpg_format *use_format = &gpg_format[0 ];
@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)if (!strcmp (var,"gpg.x509.program" )) fmtname ="x509" ; +if (!strcmp (var,"gpg.ssh.program" )) + fmtname ="ssh" ; +if (fmtname) { fmt =get_format_by_name (fmtname);return git_config_string (&fmt->program, var, value);
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigreturn use_format->sign_buffer (buffer, signature, signing_key); } +/* +* Strip CR from the line endings, in case we are on Windows. +* NEEDSWORK: make it trim only CRs before LFs and rename +*/ +static void remove_cr_after (struct strbuf *buffer,size_t offset) +{ +size_t i, j; + +for (i = j = offset; i < buffer->len; i++) { +if (buffer->buf[i] !='\r' ) { +if (i != j) + buffer->buf[j] = buffer->buf[i]; + j++; + } + } +strbuf_setlen (buffer, j); +} +static int sign_buffer_gpg (struct strbuf *buffer,struct strbuf *signature,const char *signing_key) {struct child_process gpg =CHILD_PROCESS_INIT ;int ret; -size_t i, j, bottom; +size_t bottom;struct strbuf gpg_status =STRBUF_INIT ; strvec_pushl(&gpg.args,
@@ -494,13 +531,97 @@ static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,return error (_ ("gpg failed to sign the data" ));/* Strip CR from the line endings, in case we are on Windows. */ -for (i = j = bottom; i < signature->len; i++) -if (signature->buf[i] !='\r' ) { -if (i != j) - signature->buf[j] = signature->buf[i]; - j++; - } -strbuf_setlen (signature, j); +remove_cr_after (signature, bottom);return 0 ; } + +static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key) +{ +struct child_process signer =CHILD_PROCESS_INIT ; +int ret =-1 ; +size_t bottom, keylen; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *key_file =NULL , *buffer_file =NULL ; +char *ssh_signing_key_file =NULL ; +struct strbuf ssh_signature_filename =STRBUF_INIT ; + +if (!signing_key || signing_key[0 ] =='\0' ) +return error ( +_ ("user.signingkey needs to be set for ssh signing" )); + +if (starts_with (signing_key,"ssh-" )) { +/* A literal ssh key */ + key_file =mks_tempfile_t (".git_signing_key_tmpXXXXXX" ); +if (!key_file) +return error_errno ( +_ ("could not create temporary file" )); + keylen =strlen (signing_key); +if (write_in_full (key_file->fd, signing_key, keylen) <0 || +close_tempfile_gently (key_file) <0 ) { +error_errno (_ ("failed writing ssh signing key to '%s'" ), + key_file->filename.buf); + goto out; + } + ssh_signing_key_file = 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" )); + goto out; + } + +if (write_in_full (buffer_file->fd, buffer->buf, buffer->len) <0 || +close_tempfile_gently (buffer_file) <0 ) { +error_errno (_ ("failed writing ssh signing key buffer to '%s'" ), + buffer_file->filename.buf); + goto out; + } + +strvec_pushl (&signer.args, use_format->program, +"-Y" ,"sign" , +"-n" ,"git" , +"-f" , ssh_signing_key_file, + buffer_file->filename.buf, +NULL ); + +sigchain_push (SIGPIPE ,SIG_IGN ); + ret =pipe_command (&signer,NULL ,0 ,NULL ,0 , &signer_stderr,0 ); +sigchain_pop (SIGPIPE ); + +if (ret) { +if (strstr (signer_stderr.buf,"usage:" )) +error (_ ("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)" )); + +error ("%s" , signer_stderr.buf); + goto out; + } + + bottom = signature->len; + +strbuf_addbuf (&ssh_signature_filename, &buffer_file->filename); +strbuf_addstr (&ssh_signature_filename,".sig" ); +if (strbuf_read_file (signature, ssh_signature_filename.buf,0 ) <0 ) { +error_errno ( +_ ("failed reading ssh signing data buffer from '%s'" ), + ssh_signature_filename.buf); + } +unlink_or_warn (ssh_signature_filename.buf); + +/* Strip CR from the line endings, in case we are on Windows. */ +remove_cr_after (signature, bottom); + +out: +if (key_file) +delete_tempfile (&key_file); +if (buffer_file) +delete_tempfile (&buffer_file); +strbuf_release (&signer_stderr); +strbuf_release (&ssh_signature_filename); +return ret; +}
Signature · 1 line
gitgitgadget
PATCH v6 3/9 ssh signing: retrieve a default key from ssh-agent
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
if user.signingkey is not set and a ssh signature is requested we call ssh-add -L and use the first key we get
--- gpg-interface.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-)
gpg-interface.c +25 -1
@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +/* 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 "" ; +} +const char *get_signing_key (void ) {if (configured_signing_key)return configured_signing_key; -return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); +if (!strcmp (use_format->name,"ssh" )) { +return get_default_ssh_signing_key (); + }else { +return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); + } }int sign_buffer (struct strbuf *buffer,struct strbuf *signature,const char *signing_key)
Signature · 1 line
gitgitgadget
PATCH v6 5/9 ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- builtin/receive-pack.c | 2 + gpg-interface.c | 179 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 1 deletion(-)
builtin/receive-pack.c +2 -0
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) {int status =parse_hide_refs_config (var, value,"receive" ); +git_gpg_config (var, value,NULL ); +if (status)return status;
gpg-interface.c +178 -1
214 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v6 6/9 ssh signing: add test prereqs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
generate some ssh keys and a allowedSignersFile for testing
--- t/lib-gpg.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)
t/lib-gpg.sh +29 -0
@@ -87,6 +87,35 @@ test_lazy_prereq RFC1991 ' echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null ' +test_lazy_prereq GPGSSH ' + ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1) + test $? != 127 || exit 1 + echo $ssh_version | grep -q "find-principals:missing signature file" + test $? = 0 || exit 1; + mkdir -p "${GNUPGHOME}" && + chmod 0700 "${GNUPGHOME}" && + 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 +' + +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" + sanitize_pgp() { perl -ne ' /^-----END PGP/ and $in_pgp = 0;
Signature · 1 line
gitgitgadget
PATCH v6 4/9 ssh signing: provide a textual representation of the signing key
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
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.
--- gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ gpg-interface.h | 6 ++++++ send-pack.c | 8 ++++---- 3 files changed, 56 insertions(+), 4 deletions(-)
gpg-interface.c +46 -0
@@ -470,6 +470,41 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +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; + +/* +* With 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 ]) +die_errno (_ ("failed to get the ssh fingerprint for key '%s'" ), + signing_key); + +return strbuf_detach (fingerprint[1 ],NULL ); +} +/* Returns the first public key from an ssh-agent to use for signing */ static char *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 */ +const char *get_signing_key_id (void ) +{ +if (!strcmp (use_format->name,"ssh" )) { +return get_ssh_key_fingerprint (get_signing_key ()); + }else { +/* GPG/GPGSM only store a key id on this variable */ +return get_signing_key (); + } +} +const char *get_signing_key (void ) {if (configured_signing_key)
gpg-interface.h +6 -0
@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); + +/* +* Returns a textual unique representation of the signing key in use +* Either a GPG KeyID or a SSH Key Fingerprint +*/ +const char *get_signing_key_id (void );int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc);
send-pack.c +4 -4
@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf, {const struct ref *ref;struct string_list_item *item; -char *signing_key =xstrdup (get_signing_key ()); +char *signing_key_id =xstrdup (get_signing_key_id ());const char *cp, *np;struct strbuf cert =STRBUF_INIT ;int update_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) goto free_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);return update_seen; }
Signature · 1 line
gitgitgadget
PATCH v6 8/9 ssh signing: add more tests for logs, tags & push certs
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t4202-log.sh | 23 +++++ t/t5534-push-signed.sh | 101 +++++++++++++++++++ t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100755 t/t7031-verify-tag-signed-ssh.sh
t/t4202-log.sh +23 -0
@@ -1616,6 +1616,16 @@ test_expect_success GPGSM 'setup signed branch x509' ' git commit -S -m signed_commit ' +test_expect_success GPGSSH 'setup sshkey signed branch' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_when_finished "git reset --hard && git checkout main" && + git checkout -b signed-ssh main && + echo foo >foo && + git add foo && + git commit -S -m signed_commit +' + test_expect_success GPGSM 'log x509 fingerprint' ' echo "F8BF62E0693D0694816377099909C779FA23FD65 | " >expect && git log -n1 --format="%GF | %GP" signed-x509 >actual &&
@@ -1628,6 +1638,13 @@ test_expect_success GPGSM 'log OpenPGP fingerprint' ' test_cmp expect actual ' +test_expect_success GPGSSH 'log ssh key fingerprint' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2\" | \"}" >expect && + git log -n1 --format="%GF | %GP" signed-ssh >actual && + test_cmp expect actual +' + test_expect_success GPG 'log --graph --show-signature' ' git log --graph --show-signature -n1 signed >actual && grep "^| gpg: Signature made" actual &&
@@ -1640,6 +1657,12 @@ test_expect_success GPGSM 'log --graph --show-signature x509' ' grep "^| gpgsm: Good signature" actual ' +test_expect_success GPGSSH 'log --graph --show-signature ssh' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git log --graph --show-signature -n1 signed-ssh >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + test_expect_success GPG 'log --graph --show-signature for merged tag' ' test_when_finished "git reset --hard && git checkout main" && git checkout -b plain main &&
t/t5534-push-signed.sh +101 -0
113 lines not on this page · raw message
t/t7031-verify-tag-signed-ssh.sh new file +161 -0
161 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v6 7/9 ssh signing: duplicate t7510 tests for commits
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100755 t/t7528-signed-commit-ssh.sh
t/t7528-signed-commit-ssh.sh new file +398 -0
@@ -0,0 +1,398 @@ +#!/bin/sh + +test_description='ssh signed commit tests' +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh +GNUPGHOME_NOT_USED=$GNUPGHOME +. "$TEST_DIRECTORY/lib-gpg.sh" + +test_expect_success GPGSSH 'create signed commits' ' + test_oid_cache <<-\EOF && + header sha1:gpgsig + header sha256:gpgsig-sha256 + EOF + + test_when_finished "test_unconfig commit.gpgsign" && + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + + echo 1 >file && git add file && + test_tick && git commit -S -m initial && + git tag initial && + git branch side && + + echo 2 >file && test_tick && git commit -a -S -m second && + git tag second && + + git checkout side && + echo 3 >elif && git add elif && + test_tick && git commit -m "third on side" && + + git checkout main && + test_tick && git merge -S side && + git tag merge && + + echo 4 >file && test_tick && git commit -a -m "fourth unsigned" && + git tag fourth-unsigned && + + test_tick && git commit --amend -S -m "fourth signed" && + git tag fourth-signed && + + git config commit.gpgsign true && + echo 5 >file && test_tick && git commit -a -m "fifth signed" && + git tag fifth-signed && + + git config commit.gpgsign false && + echo 6 >file && test_tick && git commit -a -m "sixth" && + git tag sixth-unsigned && + + git config commit.gpgsign true && + echo 7 >file && test_tick && git commit -a -m "seventh" --no-gpg-sign && + git tag seventh-unsigned && + + test_tick && git rebase -f HEAD^^ && git tag sixth-signed HEAD^ && + git tag seventh-signed && + + echo 8 >file && test_tick && git commit -a -m eighth -S"${SIGNING_KEY_UNTRUSTED}" && + git tag eighth-signed-alt && + + # commit.gpgsign is still on but this must not be signed + echo 9 | git commit-tree HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag ninth-unsigned $(cat oid) && + # explicit -S of course must sign. + echo 10 | git commit-tree -S HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag tenth-signed $(cat oid) && + + # --gpg-sign[=<key-id>] must sign. + echo 11 | git commit-tree --gpg-sign HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag eleventh-signed $(cat oid) && + echo 12 | git commit-tree --gpg-sign="${SIGNING_KEY_UNTRUSTED}" HEAD^{tree} >oid && + test_line_count = 1 oid && + git tag twelfth-signed-alt $(cat oid) +' + +test_expect_success GPGSSH 'verify and show signatures' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.mintrustlevel UNDEFINED && + ( + for commit in initial second merge fourth-signed \ + fifth-signed sixth-signed seventh-signed tenth-signed \ + eleventh-signed + do + git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned \ + seventh-unsigned ninth-unsigned + do + test_must_fail git verify-commit $commit && + git show --pretty=short --show-signature $commit >actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt twelfth-signed-alt + do + git show --pretty=short --show-signature $commit >actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'verify-commit exits success on untrusted signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git verify-commit eighth-signed-alt 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + grep "${KEY_NOT_TRUSTED}" actual +' + +test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.minTrustLevel fully && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits success with low minTrustLevel' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config gpg.minTrustLevel marginal && + git verify-commit sixth-signed +' + +test_expect_success GPGSSH 'verify-commit exits failure with high minTrustLevel' ' + test_config gpg.minTrustLevel ultimate && + test_must_fail git verify-commit eighth-signed-alt +' + +test_expect_success GPGSSH 'verify signatures with --raw' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + ( + for commit in initial second merge fourth-signed fifth-signed sixth-signed seventh-signed + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in merge^2 fourth-unsigned sixth-unsigned seventh-unsigned + do + test_must_fail git verify-commit --raw $commit 2>actual && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) && + ( + for commit in eighth-signed-alt + do + git verify-commit --raw $commit 2>actual && + grep "${GOOD_SIGNATURE_UNTRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual && + echo $commit OK || exit 1 + done + ) +' + +test_expect_success GPGSSH 'proper header is used for hash algorithm' ' + git cat-file commit fourth-signed >output && + grep "^$(test_oid header) -----BEGIN SSH SIGNATURE-----" output +' + +test_expect_success GPGSSH 'show signed commit with signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git show -s initial >commit && + git show -s --show-signature initial >show && + git verify-commit -v initial >verify.1 2>verify.2 && + git cat-file commit initial >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_cmp show.commit commit && + test_cmp show.gpg verify.2 && + test_cmp cat.commit verify.1 +' + +test_expect_success GPGSSH 'detect fudged signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git cat-file commit seventh-signed >raw && + sed -e "s/^seventh/7th forged/" raw >forged1 && + git hash-object -w -t commit forged1 >forged1.commit && + test_must_fail git verify-commit $(cat forged1.commit) && + git show --pretty=short --show-signature $(cat forged1.commit) >actual1 && + grep "${BAD_SIGNATURE}" actual1 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual1 && + ! grep "${GOOD_SIGNATURE_UNTRUSTED}" actual1 +' + +test_expect_success GPGSSH 'detect fudged signature with NUL' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git cat-file commit seventh-signed >raw && + cat raw >forged2 && + echo Qwik | tr "Q" "\000" >>forged2 && + git hash-object -w -t commit forged2 >forged2.commit && + test_must_fail git verify-commit $(cat forged2.commit) && + git show --pretty=short --show-signature $(cat forged2.commit) >actual2 && + grep "${BAD_SIGNATURE}" actual2 && + ! grep "${GOOD_SIGNATURE_TRUSTED}" actual2 +' + +test_expect_success GPGSSH 'amending already signed commit' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + git checkout fourth-signed^0 && + git commit --amend -S --no-edit && + git verify-commit HEAD && + git show -s --show-signature HEAD >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual && + ! grep "${BAD_SIGNATURE}" actual +' + +test_expect_success GPGSSH 'show good signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") && + cat >expect.tmpl <<-\EOF && + G + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect && + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" sixth-signed >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show bad signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect <<-\EOF && + B + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat forged1.commit) >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with custom format' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + U + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with undefined trust level' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + undefined + FINGERPRINT + + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show untrusted signature with ultimate trust level' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + cat >expect.tmpl <<-\EOF && + fully + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + git log -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_cmp expect actual +' + +test_expect_success GPGSSH 'show lack of signature with custom format' ' + cat >expect <<-\EOF && + N + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" seventh-unsigned >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' ' + test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" && + test_config log.showsignature true && + git show initial >actual && + grep "${GOOD_SIGNATURE_TRUSTED}" actual +' + +test_expect_success GPGSSH 'check config gpg.format values' ' + test_config gpg.format ssh && + test_config user.signingkey "${SIGNING_KEY_PRIMARY}" && + test_config gpg.format ssh && + git commit -S --amend -m "success" && + test_config gpg.format OpEnPgP && + test_must_fail git commit -S --amend -m "fail" +' + +test_expect_failure GPGSSH '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_oid header)//;s/^ //" | gpg --dearmor >double-sig1.sig && + gpg -o double-sig2.sig -u 29472784 --detach-sign double-base && + cat double-sig1.sig double-sig2.sig | gpg --enarmor >double-combined.asc && + sed -e "s/^\(-.*\)ARMORED FILE/\1SIGNATURE/;1s/^/$(test_oid header) /;2,\$s/^/ /" \ + double-combined.asc > double-gpgsig && + sed -e "/committer/r double-gpgsig" double-base >double-commit && + git hash-object -w -t commit double-commit >double-commit.commit && + test_must_fail git verify-commit $(cat double-commit.commit) && + git show --pretty=short --show-signature $(cat double-commit.commit) >double-actual && + grep "BAD signature from" double-actual && + grep "Good signature from" double-actual +' + +test_expect_failure GPGSSH 'show double signature with custom format (TODO)' ' + cat >expect <<-\EOF && + E + + + + + EOF + git log -1 --format="%G?%n%GK%n%GS%n%GF%n%GP" $(cat double-commit.commit) >actual && + test_cmp expect actual +' + + +test_expect_failure GPGSSH 'verify-commit verifies multiply signed commits (TODO)' ' + git init multiply-signed && + cd multiply-signed && + test_commit first && + echo 1 >second && + git add second && + tree=$(git write-tree) && + parent=$(git rev-parse HEAD^{commit}) && + git commit --gpg-sign -m second && + git cat-file commit HEAD && + # Avoid trailing whitespace. + sed -e "s/^Q//" -e "s/^Z/ /" >commit <<-EOF && + Qtree $tree + Qparent $parent + Qauthor A U Thor <[email protected]> 1112912653 -0700 + Qcommitter C O Mitter <[email protected]> 1112912653 -0700 + Qgpgsig -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC + Q AKCTqBh+VabJceXcGIZuF0Ry+udbBQ== + Q =tQ0N + Q -----END PGP SIGNATURE----- + Qgpgsig-sha256 -----BEGIN PGP SIGNATURE----- + QZ + Q iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy + Q QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO + Q AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw== + Q =pIwP + Q -----END PGP SIGNATURE----- + Q + Qsecond + EOF + head=$(git hash-object -t commit -w commit) && + git reset --hard $head && + git verify-commit $head 2>actual && + grep "Good signature from" actual && + ! grep "BAD signature from" actual +' + +test_done
Signature · 1 line
gitgitgadget
PATCH v6 9/9 ssh signing: add documentation
In reply to Fabian Stelzer via GitGitGadgetFrom: Fabian Stelzer <[email protected]>
--- Documentation/config/gpg.txt | 39 +++++++++++++++++++++++++++++++++-- Documentation/config/user.txt | 6 ++++++ 2 files changed, 43 insertions(+), 2 deletions(-)
Documentation/config/gpg.txt +37 -2
@@ -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.: [email protected],[email protected] 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.
Documentation/config/user.txt +6 -0
@@ -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.
Signature · 1 line
gitgitgadget
Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Fabian Stelzer via GitGitGadgetQuoted 9 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
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 <[email protected]>--- gpg-interface.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-)
I would have expected that this also would become a method call into *use_format object (instead of dispatching on use_format->name), but let's not go overboard. I think this is good enough for now.
Quoted 41 lines
gpg-interface.c +25
-1@@ -470,11 +470,35 @@ int git_gpg_config(const char *var, const char *value, void *cb)return 0 ; } +/* 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 "" ; +} +const char *get_signing_key (void ) {if (configured_signing_key)return configured_signing_key; -return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); +if (!strcmp (use_format->name,"ssh" )) { +return get_default_ssh_signing_key (); + }else { +return git_committer_info (IDENT_STRICT |IDENT_NO_DATE ); + } }int sign_buffer (struct strbuf *buffer,struct strbuf *signature,const char *signing_key)
Re: [PATCH v6 4/9] ssh signing: provide a textual representation of the signing key
In reply to Fabian Stelzer via GitGitGadgetQuoted 4 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
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.
These sentences that lack the initial capital letters would look unusual and distracting in our "git log --no-merges" stream.
Quoted 39 lines
gpg-interface.c +35
-0+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; + +/* +* With 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 ]) +die_errno (_ ("failed to get the ssh fingerprint for key '%s'" ), + signing_key); + +return strbuf_detach (fingerprint[1 ],NULL ); +} +/* Returns the first public key from an ssh-agent to use for signing */ static char *get_default_ssh_signing_key (void ) {
gpg-interface.c static char *get_default_ssh_signing_key(void) · line 528
Quoted 4 lines
gpg-interface.c +1
-0@@ -490,6 +525,17 @@ static char *get_default_ssh_signing_key(void)return "" ; } +/* Returns a textual but unique representation ot the signing key */
"ot" -> "of".
gpg-interface.c static char *get_default_ssh_signing_key(void) · line 535
Quoted 7 lines
gpg-interface.c +7
-0+const char *get_signing_key_id (void ) +{ +if (!strcmp (use_format->name,"ssh" )) { +return get_ssh_key_fingerprint (get_signing_key ()); + }else { +/* GPG/GPGSM only store a key id on this variable */ +return get_signing_key ();
Hmph, we could ask gpg key fingerprint if we wanted to, and we cannot tell why "ssh" side needs a separate "key" and "key_id" while "gpg" side does not. Hopefully it will become clear as we read on?
Again, dispatching on use_format->name looked rather unexpected.
Quoted 30 lines
gpg-interface.c +3
-0+ } +} +const char *get_signing_key (void ) {if (configured_signing_key)gpg-interface.h +6
-0@@ -64,6 +64,12 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature,int git_gpg_config (const char *,const char *,void *);void set_signing_key (const char *);const char *get_signing_key (void ); + +/* +* Returns a textual unique representation of the signing key in use +* Either a GPG KeyID or a SSH Key Fingerprint +*/ +const char *get_signing_key_id (void );int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc);send-pack.c +0
-0@@ -341,13 +341,13 @@ static int generate_push_cert(struct strbuf *req_buf, {const struct ref *ref;
send-pack.c static int generate_push_cert(struct strbuf *req_buf, · line 350
Quoted 10 lines
send-pack.c +2
-2struct string_list_item *item; -char *signing_key =xstrdup (get_signing_key ()); +char *signing_key_id =xstrdup (get_signing_key_id ());const char *cp, *np;struct strbuf cert =STRBUF_INIT ;int update_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);
Ahh... We do not send GPG fingerprint in push certificate but you want to use the fingerprint when signing with SSH keys, and that is where the need for signing_key_id comes from?
OK.
Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian Stelzer via GitGitGadgetQuoted 3 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
to verify a ssh signature we first call ssh-keygen -Y find-principal to
"to" -> "To".
Quoted 21 lines
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.
Quoted 5 lines
Signed-off-by Fabian Stelzer <[email protected]>--- builtin/receive-pack.c | 2 + gpg-interface.c | 179 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 1 deletion(-)
A lot of additions to support a new system, all looking quite straight-forward.
gpg-interface.c static struct gpg_format gpg_format[] = { · line 90
Quoted 9 lines
gpg-interface.c +1
-1@@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = { .program = "ssh-keygen", .verify_args = ssh_verify_args, .sigs = ssh_sigs, - .verify_signed_buffer = NULL, /* TODO */ + .verify_signed_buffer = verify_ssh_signed_buffer, .sign_buffer = sign_buffer_ssh }, };
Nice.
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 359
Quoted 12 lines
gpg-interface.c +8
-0@@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,return ret; } +static void parse_ssh_output (struct signature_check *sigc) +{ +const char *line, *principal, *search; + +/* +* ssh-keysign output should be: +* Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT +* Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT
A bit unfortunate line that is overly long. These two are not mutually exclusive two different choices, but one is a special case of the other, no? How about phrasing it like so instead?
/*
* ssh-keysign output should be:
* Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT *
* or for valid but unknown keys:
* Good "git" signature with RSA key SHA256:FINGERPRINT
*
* Note that "PRINCIPAL" can contain whitespace, "RSA" and
* "SHA256" part could be a different token that names of
* the algorithms used, and "FINGERPRINT" is a hexadecimal
* string. By finding the last occurence of " with ", we can
* reliably parse out the PRINCIPAL.
*/
Quoted 12 lines
gpg-interface.c +12
-0+* or for valid but unknown keys: +* Good "git" signature with RSA key SHA256:FINGERPRINT +*/ + sigc->result ='B' ; + sigc->trust_level =TRUST_NEVER ; + + line =xmemdupz (sigc->output,strcspn (sigc->output,"\n" )); + +if (skip_prefix (line,"Good \"git\" signature for " , &line)) { +/* Valid signature and known principal */ + sigc->result ='G' ; + sigc->trust_level =TRUST_FULLY ;
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 381
Quoted 10 lines
gpg-interface.c +10
-0+ +/* Search for the last "with" to get the full principal */ + principal = line; +do { + search =strstr (line," with " ); +if (search) + line = search +1 ; + }while (search !=NULL ); + sigc->signer =xmemdupz (principal, line - principal -1 ); + sigc->fingerprint =xstrdup (strstr (line,"key" ) +4 );
OK. This does not care the "RSA" part, which is future resistant. It assumes the <algo>:<fingerprint> comes after literal " key ", which I think is a reasonable thing to do.
However, we never checked if the line has "key" in it, so strstr(line, "key") + 4 may not be pointing at where this code expects.
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 388
Quoted 7 lines
gpg-interface.c +7
-0+ sigc->key =xstrdup (sigc->fingerprint); + }else if (skip_prefix (line,"Good \"git\" signature with " , &line)) { +/* Valid signature, but key unknown */ + sigc->result ='G' ; + sigc->trust_level =TRUST_UNDEFINED ; + sigc->fingerprint =xstrdup (strstr (line,"key" ) +4 ); + sigc->key =xstrdup (sigc->fingerprint);
Likewise, I guess.
Quoted 64 lines
gpg-interface.c +64
-0+ } +} + +static int verify_ssh_signed_buffer (struct signature_check *sigc, +struct gpg_format *fmt,const char *payload, +size_t payload_size,const char *signature, +size_t signature_size) +{ +struct child_process ssh_keygen =CHILD_PROCESS_INIT ; +struct tempfile *buffer_file; +int ret =-1 ; +const char *line; +size_t trust_size; +char *principal; +struct strbuf ssh_keygen_out =STRBUF_INIT ; +struct strbuf ssh_keygen_err =STRBUF_INIT ; + +if (!ssh_allowed_signers) { +error (_ ("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification" )); +return -1 ; + } + + buffer_file =mks_tempfile_t (".git_vtag_tmpXXXXXX" ); +if (!buffer_file) +return error_errno (_ ("could not create temporary file" )); +if (write_in_full (buffer_file->fd, signature, signature_size) <0 || +close_tempfile_gently (buffer_file) <0 ) { +error_errno (_ ("failed writing detached signature to '%s'" ), + buffer_file->filename.buf); +delete_tempfile (&buffer_file); +return -1 ; + } + +/* Find the principal from the signers */ +strvec_pushl (&ssh_keygen.args, fmt->program, +"-Y" ,"find-principals" , +"-f" , ssh_allowed_signers, +"-s" , buffer_file->filename.buf, +NULL ); + ret =pipe_command (&ssh_keygen,NULL ,0 , &ssh_keygen_out,0 , + &ssh_keygen_err,0 ); +if (ret &&strstr (ssh_keygen_err.buf,"usage:" )) { +error (_ ("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)" )); + goto out; + } +if (ret || !ssh_keygen_out.len) { +/* We did not find a matching principal in the allowedSigners - Check +* without validation */ +child_process_init (&ssh_keygen); +strvec_pushl (&ssh_keygen.args, fmt->program, +"-Y" ,"check-novalidate" , +"-n" ,"git" , +"-s" , buffer_file->filename.buf, +NULL ); + ret =pipe_command (&ssh_keygen, payload, payload_size, + &ssh_keygen_out,0 , &ssh_keygen_err,0 ); + }else { +/* Check every principal we found (one per line) */ +for (line = ssh_keygen_out.buf; *line; + line =strchrnul (line +1 ,'\n' )) { +while (*line =='\n' ) + line++; +if (!*line) +break ;
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 462
Quoted 10 lines
gpg-interface.c +10
-0+ + trust_size =strcspn (line,"\n" ); + principal =xmemdupz (line, trust_size); + +child_process_init (&ssh_keygen); +strbuf_release (&ssh_keygen_out); +strbuf_release (&ssh_keygen_err); +strvec_push (&ssh_keygen.args, fmt->program); +/* We found principals - Try with each until we find a +* match */
/*
* Do not forget our multi-line comment
* style, please.
*/
Quoted 15 lines
gpg-interface.c +15
-0+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); + }
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 487
Quoted 10 lines
gpg-interface.c +10
-0+ } + +sigchain_push (SIGPIPE ,SIG_IGN ); + ret =pipe_command (&ssh_keygen, payload, payload_size, + &ssh_keygen_out,0 , &ssh_keygen_err,0 ); +sigchain_pop (SIGPIPE ); + +FREE_AND_NULL (principal); + + ret &=starts_with (ssh_keygen_out.buf,"Good" );
This is somewhat unusual construct in our codebase, I suspect. And probably is even wrong. Didn't you mean
if (!ret) ret = starts_with(...);
instead? Surely, when pipe_command() failed, it is likely that ssh_keygen_out may not have anything useful, and checking what the first up-to-four bytes of it contain unconditionally may be cheap enough, but the person reading the code would expect you to peek into the result only when you actually got the result, no?
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 489
Quoted 2 lines
gpg-interface.c +2
-0+if (ret ==0 ) +break ;
It's more common to do
if (!ret) break;
in our codebase; in other words, we prefer not to compare with literal 0, like "if (x == 0)" or "if (y != 0)".
Thanks.
Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up
In reply to Fabian Stelzer via GitGitGadgetI 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".
Quoted 9 lines
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 thegpg_formatstructure to call format specific signing and verification functions
- moves format detection fromverify_signed_bufferinto thecheck_signatureapi function and calls the format specific verify
- renames and wrapssign_bufferto 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.
Quoted 15 lines
gpg-interface.c +6
-0@@ -15,6 +15,12 @@ struct gpg_format {const char *program;const char **verify_args;const char **sigs; +int (*verify_signed_buffer)(struct signature_check *sigc, +struct gpg_format *fmt,const char *payload, +size_t payload_size,const char *signature, +size_t signature_size); +int (*sign_buffer)(struct strbuf *buffer,struct strbuf *signature, +const char *signing_key); };
[snip]
Quoted 14 lines
static struct
gpg_formatgpg_format[] = {gpg-interface.c +7
-5- { .name = "openpgp", .program = "gpg", - .verify_args = openpgp_verify_args, - .sigs = openpgp_sigs + { + .name = "openpgp", + .program = "gpg", + .verify_args = openpgp_verify_args, + .sigs = openpgp_sigs, + .verify_signed_buffer = verify_gpg_signed_buffer, + .sign_buffer = sign_buffer_gpg, }, - { .name = "x509", .program = "gpgsm", - .verify_args = x509_verify_args,
gpg-interface.c static const char *x509_sigs[] = { · line 68
Quoted 10 lines
gpg-interface.c +7
-1- .sigs = x509_sigs + { + .name = "x509", + .program = "gpgsm", + .verify_args = x509_verify_args, + .sigs = x509_sigs, + .verify_signed_buffer = verify_gpg_signed_buffer, + .sign_buffer = sign_buffer_gpg, }, };
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.
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 284
Quoted 7 lines
gpg-interface.c +0
-3@@ -279,10 +300,6 @@ 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);
Here is the difference in functionality that I spotted. Here, lack of fmt is fatal...
Quoted 13 lines
gpg-interface.c +1
-3@@ -309,35 +330,32 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,int check_signature (const char *payload,size_t plen,const char *signature,size_t slen,struct signature_check *sigc) { -struct strbuf gpg_output =STRBUF_INIT ; -struct strbuf gpg_status =STRBUF_INIT ; +struct gpg_format *fmt;int status; sigc->result ='N' ; sigc->trust_level =-1 ; - status = verify_signed_buffer(payload, plen, signature, slen,
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 341
Quoted 10 lines
gpg-interface.c +3
-7- &gpg_output, &gpg_status); -if (status && !gpg_output.len) - goto out; - sigc->payload =xmemdupz (payload, plen); - sigc->gpg_output =strbuf_detach (&gpg_output,NULL ); - sigc->gpg_status =strbuf_detach (&gpg_status,NULL ); -parse_gpg_output (sigc); + fmt =get_format_by_sig (signature); +if (!fmt) +return error (_ ("bad/incompatible signature '%s'" ), signature);
...but here it is not.
Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys
In reply to Fabian Stelzer via GitGitGadgetKeep the commit titles to 50 characters or fewer. E.g.:
gpg-interface: teach "ssh" gpg.format
Quoted 2 lines
implements the actual
sign_buffer_sshoperation and move some shared cleanup code into a strbuf function
Capitalization and punctuation.
Quoted 9 lines
Set gpg.format = ssh and user.signingkey to either a ssh public key string (like from an
authorized_keysfile), 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 11 lines
gpg-interface.c +7
-0@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sigreturn 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;
gpg-interface.c int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig · line 501
Quoted 10 lines
gpg-interface.c +10
-0+ +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).
Quoted 23 lines
gpg-interface.c +23
-0+static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key) +{ +struct child_process signer =CHILD_PROCESS_INIT ; +int ret =-1 ; +size_t bottom, keylen; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *key_file =NULL , *buffer_file =NULL ; +char *ssh_signing_key_file =NULL ; +struct strbuf ssh_signature_filename =STRBUF_INIT ; + +if (!signing_key || signing_key[0 ] =='\0' ) +return error ( +_ ("user.signingkey needs to be set for ssh signing" )); + +if (starts_with (signing_key,"ssh-" )) { +/* A literal ssh key */ + key_file =mks_tempfile_t (".git_signing_key_tmpXXXXXX" ); +if (!key_file) +return error_errno ( +_ ("could not create temporary file" )); + keylen =strlen (signing_key); + if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 571
Quoted 10 lines
gpg-interface.c +10
-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?
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 603
Quoted 7 lines
gpg-interface.c +7
-0+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?
Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Fabian Stelzer via GitGitGadgetQuoted 2 lines
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]
Quoted 9 lines
gpg-interface.c +9
-0+/* 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 );
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 491
Quoted 10 lines
gpg-interface.c +10
-0+ 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.)
Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian Stelzer via GitGitGadgetQuoted 4 lines
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?
Quoted 7 lines
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".
Quoted 3 lines
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).
builtin/receive-pack.c static int receive_pack_config(const char *var, const char *value, void *cb) · line 137
Quoted 12 lines
builtin/receive-pack.c +2
-0@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) {int status =parse_hide_refs_config (var, value,"receive" ); +git_gpg_config (var, value,NULL ); +if (status)return status;
Check the return value of git_gpg_config() to see if that config was processed by that function - if yes, we can return early.
builtin/receive-pack.c static int receive_pack_config(const char *var, const char *value, void *cb)
Quoted 11 lines
builtin/receive-pack.c +11
-0+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?
builtin/receive-pack.c
Quoted 2 lines
builtin/receive-pack.c +2
-0+ sigc->result ='B' ; + sigc->trust_level =TRUST_NEVER ;
A discussion of trust levels should also be in the commit message or user documentation.
builtin/receive-pack.c
Quoted 1 line
builtin/receive-pack.c +1
-0+if (!strcmp (var,"gpg.ssh.revocationFile" )) {
The "F" in "revocationFile" has to be lowercase. (If tests were included, as I suggested above, it might have been easier to catch this.)
Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up
In reply to Jonathan TanQuoted 7 lines · Jonathan Tan
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 284
Quoted 3 lines
gpg-interface.c +0
-3- fmt =get_format_by_sig (signature); -if (!fmt) -BUG ("bad signature '%s'" , signature);Here is the difference in functionality that I spotted. Here, lack of fmt is fatal...
Quoted 5 lines
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 341
Quoted 3 lines
gpg-interface.c +3
-0+ fmt =get_format_by_sig (signature); +if (!fmt) +return error (_ ("bad/incompatible signature '%s'" ), signature);...but here it is not.
While I was reviewing this step, I was assumign that the callers would respond to this error return appropriately. If it is not the case, then we do have to fix that.
The original's use of BUG() is wrong in any case, I woud think. The "signature" there is an external input, so we were reporting a data error (it should have been die()), not a program logic error.
Thanks.
Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys
In reply to Jonathan TanQuoted 13 lines · Jonathan Tan
Quoted 13 lines
gpg-interface.c +13
-0+/* +* 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++;
Quoted 8 lines
gpg-interface.c int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig · line 501
Quoted 4 lines
gpg-interface.c +4
-0+ } + } +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).
You have me to blame for that "after". It was meant to signal that CR's before the given "offset" are retained.
Quoted 8 lines
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?
Sensible simplification, I guess.
Thanks for a careful review.
Quoted 12 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 603
Quoted 7 lines
gpg-interface.c +7
-0+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?
Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up
In reply to Junio C HamanoQuoted 19 lines · Junio C Hamano
Quoted 7 lines · Jonathan Tan
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 284
Quoted 3 lines
gpg-interface.c +0
-3- fmt =get_format_by_sig (signature); -if (!fmt) -BUG ("bad signature '%s'" , signature);Here is the difference in functionality that I spotted. Here, lack of fmt is fatal...
Quoted 5 lines
gpg-interface.c static int verify_signed_buffer(const char *payload, size_t payload_size, · line 341
Quoted 3 lines
gpg-interface.c +3
-0+ fmt =get_format_by_sig (signature); +if (!fmt) +return error (_ ("bad/incompatible signature '%s'" ), signature);...but here it is not.
While I was reviewing this step, I was assumign that the callers would respond to this error return appropriately. If it is not the case, then we do have to fix that.
Quoted 6 lines
The original's use of
BUG()is wrong in any case, I woud think. The "signature" there is an external input, so we were reporting a data error (it should have beendie()), not a program logic error.Thanks.
My intention was to actually change this behavior (i should have made that clear in the commit message). When the current git version encounters an unknown signature format it will BUG() and leave the user with a coredump. I assume that some repos will have multiple different signature formats in their history in the future and the effect i would have liked was to only mark the commits with unknown signatures as bad/unknown when using git log --show-signature for example. I have checked all the calls to check_signature and unfortunately the result check is really inconsistent. Some ignore it completely, others check but still then dereference fields from the sigcheck struct.
So for now a die() is probably the correct way to go.
In the future we might want to differentiate between verifying a single commit (in which case we can die()) and a list of commits. Or fix all the calls to check_signature to check the return code.
Thanks
Re: [PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen
In reply to Fabian Stelzer via GitGitGadgetQuoted 9 lines · Fabian Stelzer via GitGitGadget
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?
Signature · 1 line
An old man doll... just what I always wanted! - Clara
Re: [PATCH v6 4/9] ssh signing: provide a textual representation of the signing key
In reply to Junio C HamanoQuoted 10 lines · Junio C Hamano
Quoted 4 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
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.
These sentences that lack the initial capital letters would look unusual and distracting in our "git log --no-merges" stream.
Fixed
Quoted 5 lines
Quoted 2 lines
+/* Returns a textual but unique representation ot the signing key */
"ot" -> "of".
Fixed
Quoted 13 lines
gpg-interface.c static char *get_default_ssh_signing_key(void) · line 535
Quoted 7 lines
gpg-interface.c +7
-0+const char *get_signing_key_id (void ) +{ +if (!strcmp (use_format->name,"ssh" )) { +return get_ssh_key_fingerprint (get_signing_key ()); + }else { +/* GPG/GPGSM only store a key id on this variable */ +return get_signing_key ();Hmph, we could ask gpg key fingerprint if we wanted to, and we cannot tell why "ssh" side needs a separate "key" and "
key_id" while "gpg" side does not. Hopefully it will become clear as we read on?
Quoted 2 lines
Again, dispatching on
use_format->namelooked rather unexpected.
i will put the two strcmp(ssh) ifs on my todo list to also replace with a callback function.
Quoted 14 lines
send-pack.c static int generate_push_cert(struct strbuf *req_buf, · line 350
Quoted 9 lines
send-pack.c +2
-2-char *signing_key =xstrdup (get_signing_key ()); +char *signing_key_id =xstrdup (get_signing_key_id ());const char *cp, *np;struct strbuf cert =STRBUF_INIT ;int update_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);Ahh... We do not send GPG fingerprint in push certificate but you want to use the fingerprint when signing with SSH keys, and that is where the need for
signing_key_idcomes from?
Quoted 2 lines
OK.
Previously the push certs contained the configured user.signingkey as "pusher". For gpg this is usually the key id. (e.g.: ABCDEF01)
For ssh signing this can now be a file path which would not make much sense to put into the push cert. I did not use the public ssh key since the file can also contain an encrypted private key, so i would have to ask ssh-keygen for the public key anyway. Since the ssh fingerprint more resembles the gpg key id i used it instead.
As far as i understand the actual contents of the "pusher" header is not really relevant for the push-cert. The unique nonce is important but besides that its just a signed text blob. If we don't care about having a local users file path in this header we could drop this commit.
Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up
In reply to Jonathan TanQuoted 4 lines · Jonathan Tan
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.
Quoted 4 lines
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 10 lines
Quoted 9 lines
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 thegpg_formatstructure to call format specific signing and verification functions
- moves format detection fromverify_signed_bufferinto thecheck_signatureapi function and calls the format specific verify
- renames and wrapssign_bufferto handle format specific signing logic as well
Quoted 6 lines
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?
Quoted 23 lines
[snip]
Quoted 21 lines
static struct gpg_format gpg_format[] = {gpg-interface.c +13
-6- { .name = "openpgp", .program = "gpg", - .verify_args = openpgp_verify_args, - .sigs = openpgp_sigs + { + .name = "openpgp", + .program = "gpg", + .verify_args = openpgp_verify_args, + .sigs = openpgp_sigs, + .verify_signed_buffer = verify_gpg_signed_buffer, + .sign_buffer = sign_buffer_gpg, }, - { .name = "x509", .program = "gpgsm", - .verify_args = x509_verify_args, - .sigs = x509_sigs + { + .name = "x509", + .program = "gpgsm", + .verify_args = x509_verify_args, + .sigs = x509_sigs, + .verify_signed_buffer = verify_gpg_signed_buffer,
Quoted 9 lines
gpg-interface.c static const char *x509_sigs[] = { · line 68
Quoted 3 lines
gpg-interface.c +1
-0+ .sign_buffer = sign_buffer_gpg, }, };I think that
verify_signed_bufferandsign_buffershould replaceverify_argsand 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 useverify_argsor 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.
Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Jonathan TanQuoted 11 lines · Jonathan Tan
Quoted 2 lines
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.
Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Junio C HamanoQuoted 7 lines · Junio C Hamano
Quoted 3 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
to verify a ssh signature we first call ssh-keygen -Y find-principal to
"to" -> "To".
fixed
[snip]
Quoted 9 lines
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 359
Quoted 4 lines
gpg-interface.c +4
-0+/* +* ssh-keysign output should be: +* Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT +* Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT A bit unfortunate line that is overly long. These two are not mutually exclusive two different choices, but one is a special case of the other, no? How about phrasing it like so instead?
Quoted 14 lines
/*
* ssh-keysign output should be:
* Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT *
* or for valid but unknown keys:
* Good "git" signature with RSA key SHA256:FINGERPRINT
*
* Note that "PRINCIPAL" can contain whitespace, "RSA" and
* "SHA256" part could be a different token that names of
* the algorithms used, and "FINGERPRINT" is a hexadecimal
* string. By finding the last occurence of " with ", we can
* reliably parse out the PRINCIPAL.
*/
Yes, it's a special case that makes it a bit harder to parse. I will change the comment like you suggested. That makes it clear.
Quoted 14 lines
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 381
Quoted 9 lines
gpg-interface.c +9
-0+/* Search for the last "with" to get the full principal */ + principal = line; +do { + search =strstr (line," with " ); +if (search) + line = search +1 ; + }while (search !=NULL ); + sigc->signer =xmemdupz (principal, line - principal -1 ); + sigc->fingerprint =xstrdup (strstr (line,"key" ) +4 );OK. This does not care the "RSA" part, which is future resistant. It assumes the <algo>:<fingerprint> comes after literal " key ", which I think is a reasonable thing to do.
Quoted 4 lines
However, we never checked if the line has "key" in it, so
strstr(line, "key")+ 4 may not be pointing at where this code expects.
Hmm. What would i do if i don't find "key"? Still mark the signature as valid an just leave fingerprint & key empty?
Quoted 8 lines
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 462
Quoted 2 lines
gpg-interface.c +2
-0+/* We found principals - Try with each until we find a +* match */ /*
* Do not forget our multi-line comment
* style, please.
*/
fixed. clang-format wordwrapped those :/
Quoted 6 lines
Review
Quoted 2 lines
(quoted diff) +2
-0+ + ret &= starts_with(ssh_keygen_out.buf, "Good");This is somewhat unusual construct in our codebase, I suspect. And probably is even wrong. Didn't you mean
Quoted 8 lines
if (!ret) ret = starts_with(...);instead? Surely, when
pipe_command()failed, it is likely thatssh_keygen_outmay not have anything useful, and checking what the first up-to-four bytes of it contain unconditionally may be cheap enough, but the person reading the code would expect you to peek into the result only when you actually got the result, no?
you are correct. we don't need to look at the output when the command fails.
Quoted 9 lines
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 489
Quoted 3 lines
gpg-interface.c +2
-0+if (ret ==0 ) +break ;It's more common to do
if (!ret) break;
changed.
Thanks
Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Jonathan TanQuoted 6 lines · Jonathan Tan
Quoted 4 lines
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 9 lines
Quoted 8 lines
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.
Quoted 5 lines
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 7 lines
Quoted 4 lines
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.
Quoted 6 lines
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 5 lines
builtin/receive-pack.c static int receive_pack_config(const char *var, const char *value, void *cb) · line 134
Quoted 1 line
builtin/receive-pack.c +1
-0+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 5 lines
Quoted 5 lines
gpg-interface.c +5
-0+static void parse_ssh_output (struct signature_check *sigc) +{ +const char *line, *principal, *search; + +/*
Quoted 8 lines
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 362
Quoted 6 lines
gpg-interface.c +6
-0+* 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.
Quoted 2 lines
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 ");
Quoted 7 lines
gpg-interface.c static int verify_gpg_signed_buffer(struct signature_check *sigc, · line 364
Quoted 3 lines
gpg-interface.c +2
-0+ sigc->result ='B' ; + sigc->trust_level =TRUST_NEVER ;A discussion of trust levels should also be in the commit message or user documentation.
Quoted 6 lines
gpg-interface.c int git_gpg_config(const char *var, const char *value, void *cb) · line 627
Quoted 1 line
gpg-interface.c +1
-0+if (!strcmp (var,"gpg.ssh.revocationFile" )) {The "F" in "revocationFile" has to be lowercase. (If tests were included, as I suggested above, it might have been easier to catch this.)
fixed
Thanks
Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys
In reply to Jonathan TanQuoted 4 lines · Jonathan Tan
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 4 lines
Quoted 2 lines
implements the actual
sign_buffer_sshoperation and move some shared cleanup code into a strbuf functionCapitalization and punctuation.
fixed
Quoted 11 lines
Quoted 10 lines
Set gpg.format = ssh and user.signingkey to either a ssh public key string (like from an
authorized_keysfile), 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.
Quoted 5 lines
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.
Quoted 27 lines
Quoted 9 lines
gpg-interface.c +9
-0+ +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 ||gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 571
Quoted 10 lines
gpg-interface.c +10
-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.
Quoted 2 lines
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 4 lines
Quoted 4 lines
gpg-interface.c +4
-0+if (ret) { +if (strstr (signer_stderr.buf,"usage:" )) +error (_ ("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)" )); +
Quoted 9 lines
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 603
Quoted 3 lines
gpg-interface.c +3
-0+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.
Re: [PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen
In reply to Bagas SanjayaQuoted 11 lines · Bagas Sanjaya
Quoted 9 lines · Fabian Stelzer via GitGitGadget
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.
Quoted 2 lines
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.
Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian StelzerQuoted 6 lines · Fabian Stelzer
Quoted 5 lines · Jonathan Tan
Quoted 4 lines
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.
Quoted 7 lines
Quoted 1 line
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".
Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys
In reply to Fabian Stelzer via GitGitGadgetThanks 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]
Quoted 19 lines
gpg-interface.c +19
-0+static int sign_buffer_ssh (struct strbuf *buffer,struct strbuf *signature, +const char *signing_key) +{ +struct child_process signer =CHILD_PROCESS_INIT ; +int ret =-1 ; +size_t bottom, keylen; +struct strbuf signer_stderr =STRBUF_INIT ; +struct tempfile *key_file =NULL , *buffer_file =NULL ; +char *ssh_signing_key_file =NULL ; +struct strbuf ssh_signature_filename =STRBUF_INIT ; + +if (!signing_key || signing_key[0 ] =='\0' ) +return error ( +_ ("user.signingkey needs to be set for ssh signing" )); + +if (starts_with (signing_key,"ssh-" )) { +/* A literal ssh key */ + key_file =mks_tempfile_t (".git_signing_key_tmpXXXXXX" ); +if (!key_file)
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 567
Quoted 10 lines
gpg-interface.c +10
-0+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...
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 571
Quoted 4 lines
gpg-interface.c +4
-0+ }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.
Quoted 18 lines
gpg-interface.c +18
-0+ + 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,
gpg-interface.c static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature, · line 599
Quoted 10 lines
gpg-interface.c +10
-0+ 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?
Quoted 31 lines
gpg-interface.c +28
-0+ +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; +}Signature · 1 line
gitgitgadget
Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Fabian StelzerQuoted 19 lines · Fabian Stelzer
Quoted 11 lines · Jonathan Tan
Quoted 2 lines
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.
Quoted 6 lines
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?
Re: [PATCH v6 6/9] ssh signing: add test prereqs
In reply to Fabian Stelzer via GitGitGadgetQuoted 36 lines · Fabian Stelzer via GitGitGadget
From: Fabian Stelzer <[email protected]>
generate some ssh keys and a allowedSignersFile for testing
Signed-off-by Fabian Stelzer <[email protected]>--- t/lib-gpg.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)t/lib-gpg.sh +19
-0@@ -87,6 +87,35 @@ test_lazy_prereq RFC1991 ' echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null ' +test_lazy_prereq GPGSSH ' + ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1) + test $? != 127 || exit 1 + echo $ssh_version | grep -q "find-principals:missing signature file" + test $? = 0 || exit 1; + mkdir -p "${GNUPGHOME}" && + chmod 0700 "${GNUPGHOME}" && + 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 +' + +SIGNING_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key" +SIGNING_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key"
t/lib-gpg.sh test_lazy_prereq RFC1991 ' · line 118
Quoted 10 lines
t/lib-gpg.sh +10
-0+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.
Quoted 6 lines
t/lib-gpg.sh +0
-0sanitize_pgp() { perl -ne ' /^-----END PGP/ and $in_pgp = 0;Signature · 1 line
gitgitgadget
Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent
In reply to Josh SteadmonQuoted 3 lines · Josh Steadmon
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?
Interesting. But is it too much trouble to find out the string that is used to identify the ssh key you want to use to sign, which would make it worth supporting "use-ssh-agent" feature? Unless you want to use multiple keys in a single project, and choose one of them depending on whatever condition, and find it convenient to specify the key-of-the-day by loading it to your ssh-agent, I do not quite see why you'd want to explicitly configure it to "use-ssh-agent" and not the actual key (either the textual key itself or some key-id to choose one of your keys). Care to clarify your expected use case a bit more?
Thanks.
Re: [PATCH v6 6/9] ssh signing: add test prereqs
In reply to Josh SteadmonQuoted 11 lines · Josh Steadmon
t/lib-gpg.sh test_lazy_prereq RFC1991 ' · line 118
Quoted 5 lines
...
t/lib-gpg.sh +4
-0+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.
Again, good suggestions.
Thanks for excellent reviews.
Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian StelzerQuoted 16 lines · Fabian Stelzer
Quoted 7 lines
Quoted 7 lines
gpg-interface.c +7
-0+/* 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 );Quoted 8 lines
Review
Quoted 2 lines
(quoted diff) +2
-0+ sigc->signer = xmemdupz(principal, line - principal - 1); + sigc->fingerprint = xstrdup(strstr(line, "key") + 4);OK. This does not care the "RSA" part, which is future resistant. It assumes the <algo>:<fingerprint> comes after literal " key ", which I think is a reasonable thing to do.
However, we never checked if the line has "key" in it, sostrstr(line, "key")+ 4 may not be pointing at where this code expects.
Quoted 2 lines
Hmm. What would i do if i don't find "key"? Still mark the signature as valid an just leave fingerprint & key empty?
We didn't get a satisfactory response from the ssh-keygen we expect that tells us that the external tool successfully decided that the signature is good or bad. I would feel safer if we said we did not see a good signature in such a case.
Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures
In reply to Fabian StelzerQuoted 4 lines · Fabian Stelzer
Quoted 1 line · Jonathan Tan
Also, is this output documented to be stable even across locales?
Not really :/ (it currently is not locale specific)
We probably want to defeat l10n of the message by spawning it in the C locale regardless.
Quoted 2 lines
The documentation states to only check the commands exit code. Do we trust the exit code enough to rely on it for verification?
Is the exit code sufficient to learn who signed it? Without knowing that, we cannot see if the principal is in or not in our keychain, no?
Quoted 6 lines
If so then i can move the main result and only parse the text for the signer/fingerprint info thats used in log formats. This way only the logs would break in case the output changes.
I added the output check since the gpg code did so as well:
ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
Does ssh-keygen have a mode similar to gpg's --status-fd feature where its output is geared more towards being stable and marchine parseable than being human friendly, by the way?
Thanks.