[PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

Subsystems: documentation, the rest

STALE1713d

97 messages, 10 authors, 2021-12-30 · page 1 of 2 · open the first message on its own page

[PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-06 08:19:59

From: Fabian Stelzer <redacted>

set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.

Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.

needs openssh>8.2p1

Signed-off-by: Fabian Stelzer <redacted>
---
    RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
    
    Support for using private keyfiles directly is still missing and i'm
    unsure on how to configure it or if the pubkey in the signingkey field
    is such a good idea. A SSH Fingerprint as signingkey would be nicer, but
    key lookup would be quite cumbersome. Maybe storing the fingerprint in
    signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile
    setting? As a default we could get the first ssh key from ssh-add and
    store it in the config to avoid unintentional changes of the used
    signing key. I've started with some tests for SSH Signing but having
    static private keyfiles would make this a lot easier. So still on my
    TODO.
    
    This feature makes git signing much more accessible to the average user.
    Usually they have a SSH Key for pushing code already. Using it for
    signing commits allows us to verify not only the transport but the
    pushed code as well. The allowed_signers file could be kept in the
    repository if all receives are verified (allowing only useris with valid
    signatures to add/change them) or outside if generated/managed
    differently. Tools like gitolite could optionally generate and enforce
    them from the already existing user ssh keys for example.
    
    In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
    signing/encryption and ssh keys which i think is quite common (at least
    for the email part). This way we can establish the correct trust for the
    SSH Keys without setting up a separate GPG Infrastructure (which is
    still quite painful for users) or implementing x509 signing support for
    git (which lacks good forwarding mechanisms). Using ssh agent forwarding
    makes this feature easily usable in todays development environments
    where code is often checked out in remote VMs / containers.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v1
Pull-Request: https://github.com/git/git/pull/1041

 Documentation/config/gpg.txt  |  13 ++-
 Documentation/config/user.txt |   4 +
 gpg-interface.c               | 212 ++++++++++++++++++++++++++++++----
 gpg-interface.h               |   3 +
 4 files changed, 205 insertions(+), 27 deletions(-)
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..fd71bd782ec 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -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`
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index 59aec7c3aed..1632e7b320f 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -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.
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..53504f64410 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -8,6 +8,7 @@
 #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 {
@@ -35,6 +36,14 @@ 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,
@@ -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];
@@ -144,6 +156,38 @@ 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
+
+	output = xmemdupz(sigc->gpg_status, strcspn(sigc->gpg_status, " \n"));
+	if (skip_prefix(sigc->gpg_status, "Good \"git\" signature for ", &output)) {
+		sigc->result = 'G';
+
+		next = strchrnul(output, ' ');
+		replace_cstring(&sigc->signer, output, next);
+		output = next + 1;
+		next = strchrnul(output, ' '); // 'with'
+		output = next + 1;
+		next = strchrnul(output, ' '); // KEY Type
+		output = next + 1;
+		next = strchrnul(output, ' '); // 'key'
+		output = next + 1;
+		next = strchrnul(output, ' '); // key
+		replace_cstring(&sigc->fingerprint, output, next);
+	} else {
+		sigc->result = 'B';
+	}
+
+	// SSH-Keygen prints onto stdout instead of stderr like the output code expects - so we just copy it over
+	free(sigc->gpg_output);
+	sigc->gpg_output = xmemdupz(sigc->gpg_status, strlen(sigc->gpg_status));
+}
+
 static void parse_gpg_output(struct signature_check *sigc)
 {
 	const char *buf = sigc->gpg_status;
@@ -262,11 +306,17 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,
 				struct strbuf *gpg_output,
 				struct strbuf *gpg_status)
 {
-	struct child_process gpg = CHILD_PROCESS_INIT;
+	struct child_process gpg = CHILD_PROCESS_INIT,
+			     ssh_keygen = CHILD_PROCESS_INIT;
 	struct gpg_format *fmt;
 	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;
 
 	temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
 	if (!temp)
@@ -283,24 +333,77 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,
 	if (!fmt)
 		BUG("bad signature '%s'", signature);
 
-	strvec_push(&gpg.args, fmt->program);
-	strvec_pushv(&gpg.args, fmt->verify_args);
-	strvec_pushl(&gpg.args,
-		     "--status-fd=1",
-		     "--verify", temp->filename.buf, "-",
-		     NULL);
+	if (!strcmp(use_format->name, "ssh")) {
+		// Find the principal from the  signers
+		strvec_push(&ssh_keygen.args, fmt->program);
+		strvec_pushl(&ssh_keygen.args,  "-Y", "find-principals",
+						"-f", get_ssh_allowed_signers(),
+						"-s", temp->filename.buf,
+						NULL);
+		ret = pipe_command(&ssh_keygen, NULL, 0, &principal_out, 0, &principal_err, 0);
+		if (strstr(principal_err.buf, "unknown option")) {
+			error(_("openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)"));
+		}
+		if (ret || !principal_out.len)
+			goto out;
+
+		/* Iterate over all lines */
+		for (line = principal_out.buf; *line; line = strchrnul(line + 1, '\n')) {
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
 
-	if (!gpg_status)
-		gpg_status = &buf;
+			trust_size = strcspn(line, " \n");
+			principal = xmemdupz(line, trust_size);
 
-	sigchain_push(SIGPIPE, SIG_IGN);
-	ret = pipe_command(&gpg, payload, payload_size,
-			   gpg_status, 0, gpg_output, 0);
-	sigchain_pop(SIGPIPE);
+			strvec_push(&gpg.args,fmt->program);
+			// We found principals - Try with each until we find a match
+			strvec_pushl(&gpg.args, "-Y", "verify",
+						"-n", "git",
+						"-f", get_ssh_allowed_signers(),
+						"-I", principal,
+						"-s", temp->filename.buf,
+						 NULL);
 
-	delete_tempfile(&temp);
+			if (ssh_revocation_file) {
+				strvec_pushl(&gpg.args, "-r", ssh_revocation_file, NULL);
+			}
+
+			if (!gpg_status)
+				gpg_status = &buf;
+
+			sigchain_push(SIGPIPE, SIG_IGN);
+			ret = pipe_command(&gpg, payload, payload_size,
+					   gpg_status, 0, gpg_output, 0);
+			sigchain_pop(SIGPIPE);
 
-	ret |= !strstr(gpg_status->buf, "\n[GNUPG:] GOODSIG ");
+			ret |= !strstr(gpg_status->buf, "Good");
+			if (ret == 0)
+				break;
+		}
+	} else {
+		strvec_push(&gpg.args, fmt->program);
+		strvec_pushv(&gpg.args, fmt->verify_args);
+		strvec_pushl(&gpg.args,
+				"--status-fd=1",
+				"--verify", temp->filename.buf, "-",
+				NULL);
+
+		if (!gpg_status)
+			gpg_status = &buf;
+
+		sigchain_push(SIGPIPE, SIG_IGN);
+		ret = pipe_command(&gpg, payload, payload_size, gpg_status, 0,
+				   gpg_output, 0);
+		sigchain_pop(SIGPIPE);
+		ret |= !strstr(gpg_status->buf, "\n[GNUPG:] GOODSIG ");
+	}
+
+out:
+	delete_tempfile(&temp);
+	strbuf_release(&principal_out);
+	strbuf_release(&principal_err);
 	strbuf_release(&buf); /* no matter it was used or not */
 
 	return ret;
@@ -323,7 +426,11 @@ 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);
-	parse_gpg_output(sigc);
+	if (!strcmp(use_format->name, "ssh")) {
+		parse_ssh_output(sigc);
+	} else {
+		parse_gpg_output(sigc);
+	}
 	status |= sigc->result != 'G';
 	status |= sigc->trust_level < configured_min_trust_level;
 
@@ -394,6 +501,14 @@ int git_gpg_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (!strcmp(var, "gpg.ssh.allowedsigners")) {
+		return git_config_string(&ssh_allowed_signers, var, value);
+	}
+
+	if (!strcmp(var, "gpg.ssh.revocationfile")) {
+		return git_config_string(&ssh_revocation_file, var, value);
+	}
+
 	if (!strcmp(var, "gpg.format")) {
 		if (!value)
 			return config_error_nonbool(var);
@@ -425,6 +540,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);
@@ -437,7 +555,19 @@ 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 "";
+	} 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;
+	return GPG_SSH_ALLOWED_SIGNERS;
 }
 
 int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
@@ -446,12 +576,35 @@ 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;
-
-	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)
+			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;
+		}
+		strvec_pushl(&gpg.args, use_format->program ,
+					"-Y", "sign",
+					"-n", "git",
+					"-f", temp->filename.buf,
+					NULL);
+	} else {
+		strvec_pushl(&gpg.args, use_format->program ,
+					"--status-fd=2",
+					"-bsau", signing_key,
+					NULL);
+	}
 
 	bottom = signature->len;
 
@@ -464,7 +617,16 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
 			   signature, 1024, &gpg_status, 0);
 	sigchain_pop(SIGPIPE);
 
-	ret |= !strstr(gpg_status.buf, "\n[GNUPG:] SIG_CREATED ");
+	if (temp)
+		delete_tempfile(&temp);
+
+	if (!strcmp(use_format->name, "ssh")) {
+		if (strstr(gpg_status.buf, "unknown option")) {
+			error(_("openssh version > 8.2p1 is needed for ssh signing (ssh-keygen needs -Y sign option)"));
+		}
+	} else {
+		ret |= !strstr(gpg_status.buf, "\n[GNUPG:] SIG_CREATED ");
+	}
 	strbuf_release(&gpg_status);
 	if (ret)
 		return error(_("gpg failed to sign the data"));
diff --git a/gpg-interface.h b/gpg-interface.h
index 80567e48948..286c1b4167a 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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
-- 
gitgitgadget

Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Han-Wen Nienhuys <hidden>
Date: 2021-07-06 10:07:36

On Tue, Jul 6, 2021 at 10:20 AM Fabian Stelzer via GitGitGadget
[off-list ref] wrote:
From: Fabian Stelzer <redacted>

set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.

Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
...
    In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
    signing/encryption and ssh keys which i think is quite common (at least
    for the email part). This way we can establish the correct trust for the
    SSH Keys without setting up a separate GPG Infrastructure (which is
    still quite painful for users) or implementing x509 signing support for
    git (which lacks good forwarding mechanisms). Using ssh agent forwarding
    makes this feature easily usable in todays development environments
    where code is often checked out in remote VMs / containers.
Thanks for working on this, and I support this initiative. I
coincidentally have started proselytizing something similar just weeks
ago.

My interest is in signing pushes rather than commits/tags, as that (in
combination with SSH U2F support) provides a simple mechanism to
require (forwardable!) 2-factor authentication on pushes over HTTP. I
haven't looked at the signing code in detail, but I had the impression
that adding SSH signatures would automatically also add support for
signed pushes? (aka. push-certs) Do you know?

-- 
Han-Wen Nienhuys - Google Munich
I work 80%. Don't expect answers from me on Fridays.
--

Google Germany GmbH, Erika-Mann-Strasse 33, 80636 Munich

Registergericht und -nummer: Hamburg, HRB 86891

Sitz der Gesellschaft: Hamburg

Geschäftsführer: Paul Manicle, Halimah DeLaine Prado

Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer <hidden>
Date: 2021-07-06 11:34:43

quoted
From: Fabian Stelzer <redacted>

set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.

Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.
...
quoted
     In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
     signing/encryption and ssh keys which i think is quite common (at least
     for the email part). This way we can establish the correct trust for the
     SSH Keys without setting up a separate GPG Infrastructure (which is
     still quite painful for users) or implementing x509 signing support for
     git (which lacks good forwarding mechanisms). Using ssh agent forwarding
     makes this feature easily usable in todays development environments
     where code is often checked out in remote VMs / containers.
Thanks for working on this, and I support this initiative. I
coincidentally have started proselytizing something similar just weeks
ago.

My interest is in signing pushes rather than commits/tags, as that (in
combination with SSH U2F support) provides a simple mechanism to
require (forwardable!) 2-factor authentication on pushes over HTTP. I
haven't looked at the signing code in detail, but I had the impression
that adding SSH signatures would automatically also add support for
signed pushes? (aka. push-certs) Do you know?
Up until now i was not actually aware of the "push signing" 
functionality in git.
I can see that the send/receive-pack use the same api function calls as 
commit/tag signing.
So this should work just as well. Especially if using an ssh agent the whole
process is identical to git. I still need to try private key files 
directly to see
how user interaction (like entering a passphrase or touching the U2F 
Token) would work.

Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

From: brian m. carlson <hidden>
Date: 2021-07-06 14:45:06

On 2021-07-06 at 08:19:53, Fabian Stelzer via GitGitGadget wrote:
From: Fabian Stelzer <redacted>

set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.

Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.

needs openssh>8.2p1
Usually we'll want to write the explanation here in full sentences with
typical capitalization.
Signed-off-by: Fabian Stelzer <redacted>
---
    RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
    
    Support for using private keyfiles directly is still missing and i'm
    unsure on how to configure it or if the pubkey in the signingkey field
    is such a good idea. A SSH Fingerprint as signingkey would be nicer, but
    key lookup would be quite cumbersome. Maybe storing the fingerprint in
    signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile
    setting? As a default we could get the first ssh key from ssh-add and
    store it in the config to avoid unintentional changes of the used
    signing key. I've started with some tests for SSH Signing but having
    static private keyfiles would make this a lot easier. So still on my
    TODO.
I think user.signingKey could be helpful for signing here.  That could
be a file name, not just a fingerprint, although we'd probably want to
have support for tilde expansion.  You could add an additional option,
gpg.ssh.keyring, that specifies the signatures to verify.  That would be
named the same thing as a potential option of gpg.openpgp.keyring,
which would be convenient.  Also, gpg.ssh.revokedKeyring could maybe be
the name for revoked keys?
    This feature makes git signing much more accessible to the average user.
    Usually they have a SSH Key for pushing code already. Using it for
    signing commits allows us to verify not only the transport but the
    pushed code as well. The allowed_signers file could be kept in the
    repository if all receives are verified (allowing only useris with valid
    signatures to add/change them) or outside if generated/managed
    differently. Tools like gitolite could optionally generate and enforce
    them from the already existing user ssh keys for example.
    
    In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
    signing/encryption and ssh keys which i think is quite common (at least
    for the email part). This way we can establish the correct trust for the
    SSH Keys without setting up a separate GPG Infrastructure (which is
    still quite painful for users) or implementing x509 signing support for
    git (which lacks good forwarding mechanisms). Using ssh agent forwarding
    makes this feature easily usable in todays development environments
    where code is often checked out in remote VMs / containers.
I think some of this rationale would work well in the commit message,
especially the part about the fact that using an SSH key may be easier
for users and the fact that it can be well supported by smart cards.
Those are compelling arguments about why this is a desirable change, and
should be in the commit message.

I haven't looked too deeply at the intricacies of the change, but I'm in
favor of it.  I would, however, like to see some tests here, including
for commits, tags, and push certificates.  Note that you'll probably
need to run the testsuite both with and without
GIT_TEST_DEFAULT_HASH=sha256 to verify everything works.
quoted hunk
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..fd71bd782ec 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -11,13 +11,13 @@ gpg.program::
 
 gpg.format::
 	Specifies which key format to use when signing with `--gpg-sign`.
-	Default is "openpgp" and another possible value is "x509".
+	Default is "openpgp". Other possible values are "x509", "ssh".
 
 gpg.<format>.program::
 	Use this to customize the program used for the signing format you
 	chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
 	be used as a legacy synonym for `gpg.openpgp.program`. The default
-	value for `gpg.x509.program` is "gpgsm".
+	value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
 
 gpg.minTrustLevel::
 	Specifies a minimum trust level for signature verification.  If
@@ -27,6 +27,15 @@ gpg.minTrustLevel::
 	with at least `undefined` trust.  Setting this option overrides
 	the required trust-level for all operations.  Supported values,
 	in increasing order of significance:
+
+gpg.ssh.allowedSigners::
+	A file containing all valid SSH signing principals. 
+	Similar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.
+	Defaults to .gitsigners
We probably don't want to store this in the repository.  If OpenSSH has
a standard location for this, then we can default to that; otherwise, we
should pick something in .ssh or in $XDG_CONFIG_HOME/git.
-- 
brian m. carlson (he/him or they/them)
Toronto, Ontario, CA

Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer <hidden>
Date: 2021-07-06 15:33:56

From: Fabian Stelzer <redacted>
quoted
set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.

Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.

needs openssh>8.2p1
Usually we'll want to write the explanation here in full sentences with
typical capitalization.
Thanks, i was unsure about what to put in the commit and what into the 
cover letter.
I'll fix this with the next patch update and move some of it into the 
commit message.
In our env the commit messages are usually kept quite short.
quoted
Signed-off-by: Fabian Stelzer <redacted>
---
     RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen
     
     Support for using private keyfiles directly is still missing and i'm
     unsure on how to configure it or if the pubkey in the signingkey field
     is such a good idea. A SSH Fingerprint as signingkey would be nicer, but
     key lookup would be quite cumbersome. Maybe storing the fingerprint in
     signingkey and then have a gpg.ssh.$FINGERPRINT.publickey/privatekeyfile
     setting? As a default we could get the first ssh key from ssh-add and
     store it in the config to avoid unintentional changes of the used
     signing key. I've started with some tests for SSH Signing but having
     static private keyfiles would make this a lot easier. So still on my
     TODO.
I think user.signingKey could be helpful for signing here.  That could
be a file name, not just a fingerprint, although we'd probably want to
have support for tilde expansion.  You could add an additional option,
gpg.ssh.keyring, that specifies the signatures to verify.  That would be
named the same thing as a potential option of gpg.openpgp.keyring,
which would be convenient.  Also, gpg.ssh.revokedKeyring could maybe be
the name for revoked keys?
The problem ist that looking up a key by fingerprint alone is not really 
possible with ssh :/
A referenced file (which could contain a public or private key) would be 
fine and i could return the fingerprint in the get_signing_key api which 
the pushcerts code uses as "pusher" info in the cert.
I'll change the keyring naming to what you suggested. Makes sense to 
have this option for gpg as well.
quoted
     This feature makes git signing much more accessible to the average user.
     Usually they have a SSH Key for pushing code already. Using it for
     signing commits allows us to verify not only the transport but the
     pushed code as well. The allowed_signers file could be kept in the
     repository if all receives are verified (allowing only useris with valid
     signatures to add/change them) or outside if generated/managed
     differently. Tools like gitolite could optionally generate and enforce
     them from the already existing user ssh keys for example.
     
     In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
     signing/encryption and ssh keys which i think is quite common (at least
     for the email part). This way we can establish the correct trust for the
     SSH Keys without setting up a separate GPG Infrastructure (which is
     still quite painful for users) or implementing x509 signing support for
     git (which lacks good forwarding mechanisms). Using ssh agent forwarding
     makes this feature easily usable in todays development environments
     where code is often checked out in remote VMs / containers.
I think some of this rationale would work well in the commit message,
especially the part about the fact that using an SSH key may be easier
for users and the fact that it can be well supported by smart cards.
Those are compelling arguments about why this is a desirable change, and
should be in the commit message.

I haven't looked too deeply at the intricacies of the change, but I'm in
favor of it.  I would, however, like to see some tests here, including
for commits, tags, and push certificates.  Note that you'll probably
need to run the testsuite both with and without
GIT_TEST_DEFAULT_HASH=sha256 to verify everything works.
I'm working on some tests but there are lots of GPG / GPGSM tests in the 
suite and i'm unsure of how many i should duplicate.
Thanks for the info with the hash setting.
quoted
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..fd71bd782ec 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -11,13 +11,13 @@ gpg.program::
  
  gpg.format::
  	Specifies which key format to use when signing with `--gpg-sign`.
-	Default is "openpgp" and another possible value is "x509".
+	Default is "openpgp". Other possible values are "x509", "ssh".
  
  gpg.<format>.program::
  	Use this to customize the program used for the signing format you
  	chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
  	be used as a legacy synonym for `gpg.openpgp.program`. The default
-	value for `gpg.x509.program` is "gpgsm".
+	value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
  
  gpg.minTrustLevel::
  	Specifies a minimum trust level for signature verification.  If
@@ -27,6 +27,15 @@ gpg.minTrustLevel::
  	with at least `undefined` trust.  Setting this option overrides
  	the required trust-level for all operations.  Supported values,
  	in increasing order of significance:
+
+gpg.ssh.allowedSigners::
+	A file containing all valid SSH signing principals.
+	Similar to an .ssh/authorized_keys file. See ssh-keygen(1) for details.
+	Defaults to .gitsigners
We probably don't want to store this in the repository.  If OpenSSH has
a standard location for this, then we can default to that; otherwise, we
should pick something in .ssh or in $XDG_CONFIG_HOME/git.
I'm not aware of a standard location. I think there are use cases to 
store this in the repo, but i'm of course fine not defaulting to it.

Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Bagas Sanjaya <hidden>
Date: 2021-07-07 06:27:11

On 06/07/21 15.19, Fabian Stelzer via GitGitGadget wrote:
From: Fabian Stelzer <redacted>

set gpg.format = ssh and user.signingkey to a ssh public key string (like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.

Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.

needs openssh>8.2p1
Why did you choose to implement SSH-based signing as GPG interface? Why 
not create similar one?

If at later times we need to implement other signing methods (besides 
GPG and SSH), we can refactor gpg-interface into generic signing 
interface (say `signing.h`) and let each signing methods implement from it.

-- 
An old man doll... just what I always wanted! - Clara

Re: [PATCH] Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer <hidden>
Date: 2021-07-07 08:48:55

On 07.07.21 08:26, Bagas Sanjaya wrote:
On 06/07/21 15.19, Fabian Stelzer via GitGitGadget wrote:
quoted
From: Fabian Stelzer <redacted>

set gpg.format = ssh and user.signingkey to a ssh public key string 
(like from an
authorized_keys file) and commits/tags can be signed using the private
key from your ssh-agent.

Verification uses a allowed_signers_file (see ssh-keygen(1)) which
defaults to .gitsigners but can be set via gpg.ssh.allowedsigners
A possible gpg.ssh.revocationfile is also passed to ssh-keygen on
verification.

needs openssh>8.2p1
Why did you choose to implement SSH-based signing as GPG interface? 
Why not create similar one?

If at later times we need to implement other signing methods (besides 
GPG and SSH), we can refactor gpg-interface into generic signing 
interface (say `signing.h`) and let each signing methods implement 
from it. 
I agree that a general purpose "signing" would be cleaner. The GPG 
kewords are scattered all over the codebase but all the paths i found 
just call the generic sign_buffer / verify_signed_buffer from 
gpg-interface.c in the end whose api works quite well for other signing 
mechanisms as well. I will rename some struct fields to be more generic 
and adjust a few messages printed to the user which currently say things 
like "gpg failed to sign the data" or "has a gpg signature" to be 
generic. Do we just want to call this "signature" and remove the gpg 
prefix or would that be too generic?

Refactoring the whole gpg part to a generic "signing" would be quite 
involved and should probably be a different patch even though its mostly 
renaming stuff.
If we want to go into that direction i could add the new config keys 
under signing.* (signing.format = ssh|gpg, ...) and keep the 
compatibility for the older gpg.* keys.

[PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-12 12:19:45

From: Fabian Stelzer <redacted>

Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.

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.

Signed-off-by: Fabian Stelzer <redacted>
---
    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 [off-list ref]
      
       ## 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 [off-list ref]
      
     @@ 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 hasnokey@nowhere.com &&
     ++	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 [off-list ref] 1112912653 -0700
     ++# 	Qcommitter C O Mitter [off-list ref] 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


 Documentation/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
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..c2bc4d06a66 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -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.
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index 59aec7c3aed..e71a099b8b8 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -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.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a34742513ac..fd790f7fd72 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 {
 	int status = parse_hide_refs_config(var, value, "receive");
 
+	git_gpg_config(var, value, NULL);
+
 	if (status)
 		return status;
 
@@ -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)) {
diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c
index 0f66818e0f8..1d7b64fa021 100644
--- a/fmt-merge-msg.c
+++ b/fmt-merge-msg.c
@@ -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);
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..3b1a350bcfd 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -3,11 +3,13 @@
 #include "config.h"
 #include "run-command.h"
 #include "strbuf.h"
+#include "dir.h"
 #include "gpg-interface.h"
 #include "sigchain.h"
 #include "tempfile.h"
 
 static char *configured_signing_key;
+const char *ssh_allowed_signers, *ssh_revocation_file;
 static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
 
 struct gpg_format {
@@ -35,6 +37,14 @@ 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,
@@ -44,6 +54,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,6 +85,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->output);
 	FREE_AND_NULL(sigc->gpg_output);
 	FREE_AND_NULL(sigc->gpg_status);
 	FREE_AND_NULL(sigc->signer);
@@ -144,6 +158,53 @@ 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;
@@ -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,
+	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,
+						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;
 }
@@ -309,35 +468,34 @@ 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);
@@ -388,12 +546,31 @@ 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);
@@ -425,6 +602,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);
@@ -433,11 +613,80 @@ 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)
@@ -446,25 +695,88 @@ 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"));
diff --git a/gpg-interface.h b/gpg-interface.h
index 80567e48948..03d56475b56 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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);
diff --git a/log-tree.c b/log-tree.c
index 7b823786c2c..20af9bd1c82 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -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);
diff --git a/pretty.c b/pretty.c
index b1ecd039cef..daa71394efd 100644
--- a/pretty.c
+++ b/pretty.c
@@ -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) {
diff --git a/send-pack.c b/send-pack.c
index 9cb9f716509..c8fb0c30f87 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -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;
 }
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index 9fc5241228e..96935582262 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -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;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 350cfa35936..84227066685 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -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 &&
diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh
index bba768f5ded..19b37e999b4 100755
--- a/t/t5534-push-signed.sh
+++ b/t/t5534-push-signed.sh
@@ -137,6 +137,53 @@ 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 &&
@@ -276,6 +323,61 @@ 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 hasnokey@nowhere.com &&
+	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 &&
diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh
new file mode 100755
index 00000000000..d2b7a525584
--- /dev/null
+++ b/t/t7031-verify-tag-signed-ssh.sh
@@ -0,0 +1,176 @@
+#!/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
diff --git a/t/t7527-signed-commit-ssh.sh b/t/t7527-signed-commit-ssh.sh
new file mode 100755
index 00000000000..f397a6dd327
--- /dev/null
+++ b/t/t7527-signed-commit-ssh.sh
@@ -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' '
+# 	(
+# 		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 <author@example.com> 1112912653 -0700
+# 	Qcommitter C O Mitter <committer@example.com> 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
base-commit: d486ca60a51c9cb1fe068803c3f540724e95e83a
-- 
gitgitgadget

Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen

From: Ævar Arnfjörð Bjarmason <hidden>
Date: 2021-07-12 17:18:23

On Mon, Jul 12 2021, Fabian Stelzer via GitGitGadget wrote:
quoted hunk
 gpg.format::
 	Specifies which key format to use when signing with `--gpg-sign`.
-	Default is "openpgp" and another possible value is "x509".
+	Default is "openpgp". Other possible values are "x509", "ssh".
 
 gpg.<format>.program::
 	Use this to customize the program used for the signing format you
 	chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
 	be used as a legacy synonym for `gpg.openpgp.program`. The default
-	value for `gpg.x509.program` is "gpgsm".
+	value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
 
 gpg.minTrustLevel::
 	Specifies a minimum trust level for signature verification.  If
@@ -33,3 +33,34 @@ gpg.minTrustLevel::
 * `marginal`
 * `fully`
 * `ultimate`
+
+gpg.ssh.keyring::
+	A file containing all valid SSH public signing keys. 
+	Similar to an .ssh/authorized_keys file.
+	See ssh-keygen(1) "ALLOWED SIGNERS" for details.
+	If a signing key is found in this file then the trust level will
+	be set to "fully". Otherwise if the key is not present
+	but the signature is still valid then the trust level will be "undefined".
+
+	This file can be set to a location outside of the repository
+	and every developer maintains their own trust store.
+	A central repository server could generate this file automatically
+	from ssh keys with push	access to verify the code against.
+	In a corporate setting this file is probably generated at a global location
+	from some automation that already handles developer ssh keys. 
+	
+	A repository that is only allowing signed commits can store the file 
+	in the repository itself using a relative path. This way only committers
+	with an already valid key can add or change keys in the keyring.
+
+	Using a SSH CA key with the cert-authority option 
+	(see ssh-keygen(1) "CERTIFICATES") is also valid.
+
+	To revoke a key place the public key without the principal into the 
+	revocationKeyring.
+
+gpg.ssh.revocationKeyring::
+	Either a SSH KRL or a list of revoked public keys (without the principal prefix).
+	See ssh-keygen(1) for details.
+	If a public key is found in this file then it will always be treated
+	as having trust level "never" and signatures will show as invalid.
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index 59aec7c3aed..e71a099b8b8 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -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.
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a34742513ac..fd790f7fd72 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 {
 	int status = parse_hide_refs_config(var, value, "receive");
 
+	git_gpg_config(var, value, NULL);
+
 	if (status)
 		return status;
 
@@ -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.
+static void parse_ssh_output(struct signature_check *sigc)
+{
+	const char *output = NULL;
+	char *next = NULL;
+
+	/* ssh-keysign output should be:
+	 * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
+	 * or for valid but unknown keys:
+	 * Good "git" signature with RSA key SHA256:FINGERPRINT
+	 */
Style:

 /*
  * Comments like this
  */

Not /* Comments [...]
+
+	output = xmemdupz(sigc->output, strcspn(sigc->output, " \n"));
+	if (skip_prefix(sigc->output, "Good \"git\" signature for ", &output)) {
+		// Valid signature for a trusted signer
We don't use C99 comments, so /* ... */ (but perhaps we should nowadays,
but that's another topic...).
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_FULLY;
+
+		next = strchrnul(output, ' '); // 'principal'
+		replace_cstring(&sigc->signer, output, next);
+		output = next + 1;
+		next = strchrnul(output, ' '); // 'with'
+		output = next + 1;
+		next = strchrnul(output, ' '); // KEY Type
+		output = next + 1;
+		next = strchrnul(output, ' '); // 'key'
+		output = next + 1;
FWIW for new code we'd probably use string_list_split() or
string_list_split_in_place() or strbuf_split_buf() or something, but I
see this is following the existing pattern in the file...
quoted hunk
+		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;
@@ -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 hunk
+	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,
+						NULL);
+		ret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+	} else {
+		// Check every principal we found (one per line)
+		for (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\n')) {
Hrm, can't we use strbuf_getline() here with the underlying io_pump API
that pipe_command() uses, instead of slurping it all up, and then
splitting on '\n' ourselves? (I'm not sure)
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, " \n");
+			principal = xmemdupz(line, trust_size);
+
+			child_process_init(&ssh_keygen);
+			strbuf_release(&ssh_keygen_out);
+			strbuf_release(&ssh_keygen_err);
+			strvec_push(&ssh_keygen.args,fmt->program);
+			// We found principals - Try with each until we find a match
+			strvec_pushl(&ssh_keygen.args,  "-Y", "verify",
+							//TODO: sprintf("-Overify-time=%s", commit->date...),
+							"-n", "git",
+							"-f", get_ssh_allowed_signers(),
+							"-I", principal,
+							"-s", temp->filename.buf,
+							NULL);
+
+			if (ssh_revocation_file && file_exists(ssh_revocation_file)) {
+				strvec_pushl(&ssh_keygen.args, "-r", ssh_revocation_file, NULL);
Do we want to silently ignore missing but configured revocation files?
+			}
+
+			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...
Broken how? And why the commented-out code as part of the patch?
-	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?
+static char *get_ssh_key_fingerprint(const char *signing_key) {
+	struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+	int ret = -1;
+	struct strbuf fingerprint_stdout = STRBUF_INIT;
+	struct strbuf **fingerprint;
+
+	/* For SSH Signing this can contain a filename or a public key
+	* For textual representation we usually want a fingerprint
+	*/
+	if (istarts_with(signing_key, "ssh-")) {
+		strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+					"-lf", "-",
+					NULL);
+		ret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0,  NULL, 0);
+	} else {
+		strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+					"-lf", configured_signing_key,
+					NULL);
+		ret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);
+		if (!!ret)
+			die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+		fingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);
+		if (fingerprint[1]) {
+			return strbuf_detach(fingerprint[1], NULL);
+		}
+	}
+	die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+}
Her you declare a ret that's not used at all in the "istarts_with"
branch, and we fall through to die_errno()?

[I stopped reading mostly at this point]
[...]
+# 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
+# '
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

From: Fabian Stelzer <hidden>
Date: 2021-07-12 20:36:01

On 12.07.21 18:55, Ævar Arnfjörð Bjarmason wrote:

I'll change all the whitespace / comments / style issues with the next 
commit. Thanks
quoted
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_FULLY;
+
+		next = strchrnul(output, ' '); // 'principal'
+		replace_cstring(&sigc->signer, output, next);
+		output = next + 1;
+		next = strchrnul(output, ' '); // 'with'
+		output = next + 1;
+		next = strchrnul(output, ' '); // KEY Type
+		output = next + 1;
+		next = strchrnul(output, ' '); // 'key'
+		output = next + 1;
FWIW for new code we'd probably use string_list_split() or
string_list_split_in_place() or strbuf_split_buf() or something, but I
see this is following the existing pattern in the file...
I agree. This is my first patch in the git codebase so it takes a bit 
getting used to all the available utilities.
quoted
+	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,
+						NULL);
+		ret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+	} else {
+		// Check every principal we found (one per line)
+		for (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\n')) {
Hrm, can't we use strbuf_getline() here with the underlying io_pump API
that pipe_command() uses, instead of slurping it all up, and then
splitting on '\n' ourselves? (I'm not sure)
Sounds good. I'll give it a try.
quoted
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, " \n");
+			principal = xmemdupz(line, trust_size);
+
+			child_process_init(&ssh_keygen);
+			strbuf_release(&ssh_keygen_out);
+			strbuf_release(&ssh_keygen_err);
+			strvec_push(&ssh_keygen.args,fmt->program);
+			// We found principals - Try with each until we find a match
+			strvec_pushl(&ssh_keygen.args,  "-Y", "verify",
+							//TODO: sprintf("-Overify-time=%s", commit->date...),
+							"-n", "git",
+							"-f", get_ssh_allowed_signers(),
+							"-I", principal,
+							"-s", temp->filename.buf,
+							NULL);
+
+			if (ssh_revocation_file && file_exists(ssh_revocation_file)) {
+				strvec_pushl(&ssh_keygen.args, "-r", ssh_revocation_file, NULL);
Do we want to silently ignore missing but configured revocation files?
I'll add a warning
quoted
+			}
+
+			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...
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
-	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?
The BUG() call is also from the original code. I agree that it should be 
handled differently.
Unfortunately this call is also the reason that when trying to verify a 
new SSH signature with a current git version you'll get a segfault from 
this BUG() :/
I'm not sure if i can do anything about this other than adding a 
completely new tag in the commit itself instead of "gpgsig" which might 
be quite involved. I haven't looked into that too much yet.
quoted
+static char *get_ssh_key_fingerprint(const char *signing_key) {
+	struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+	int ret = -1;
+	struct strbuf fingerprint_stdout = STRBUF_INIT;
+	struct strbuf **fingerprint;
+
+	/* For SSH Signing this can contain a filename or a public key
+	* For textual representation we usually want a fingerprint
+	*/
+	if (istarts_with(signing_key, "ssh-")) {
+		strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+					"-lf", "-",
+					NULL);
+		ret = pipe_command(&ssh_keygen, signing_key, strlen(signing_key), &fingerprint_stdout, 0,  NULL, 0);
+	} else {
+		strvec_pushl(&ssh_keygen.args, "ssh-keygen",
+					"-lf", configured_signing_key,
+					NULL);
+		ret = pipe_command(&ssh_keygen, NULL, 0, &fingerprint_stdout, 0, NULL, 0);
+		if (!!ret)
+			die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+		fingerprint = strbuf_split_max(&fingerprint_stdout, ' ', 3);
+		if (fingerprint[1]) {
+			return strbuf_detach(fingerprint[1], NULL);
+		}
+	}
+	die_errno(_("failed to get the ssh fingerprint for key '%s'"), signing_key);
+}
Her you declare a ret that's not used at all in the "istarts_with"
branch, and we fall through to die_errno()?
I'll clean up the logic. Thanks
[I stopped reading mostly at this point]
quoted
[...]
+# 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
+# '
Perhaps you're looking for test_expect_failure for TODO tests?
Yes. Although this test explicitly i'm having a hard time to duplicate 
for ssh. I'm still trying to find out if the duplicate signature thing 
is actually an issue with ssh.
I think this patch is *way* past the point of benefitting from being
split into a patch series. It grew from ~200 lines added to ~1k.
Sure, I can easily split the patch into seperate commits. But do i 
create a v3 patch from this or issue a new pull request?
The diff between v2 & v3 would be quite useless otherwise wouldn't it?

And maybe another beginner contribution question:

When i make changes to a patchset do i put new changes from the review 
on top as new commits or do i edit the existing commits?
If so what is the workflow you normally use for this? fixup commits? I 
know about those but haven't worked with them before.

Thanks for your help!

Re: [PATCH v2] Add commit, tag & push signing/verification via SSH keys using ssh-keygen

From: Felipe Contreras <hidden>
Date: 2021-07-12 21:16:56

Fabian Stelzer wrote:
On 12.07.21 18:55, Ævar Arnfjörð Bjarmason wrote:
quoted
I think this patch is *way* past the point of benefitting from being
split into a patch series. It grew from ~200 lines added to ~1k.
Sure, I can easily split the patch into seperate commits. But do i 
create a v3 patch from this or issue a new pull request?
The diff between v2 & v3 would be quite useless otherwise wouldn't it?
The interdiff might be quite useless, but not the rangediff. Either way
both of those are merely tools to visualize changes between versions,
ultimately what really matters is the final commits themselves.

Moreover, not all reviewers have seen every version, so for example if
you properly split this patch, I might join the review process at v3,
and I don't really care what was in v2, therefore I wouldn't look at the
rangediff.
And maybe another beginner contribution question:

When i make changes to a patchset do i put new changes from the review 
on top as new commits or do i edit the existing commits?
Edit existing commits.
If so what is the workflow you normally use for this? fixup commits? I 
know about those but haven't worked with them before.
`git rebase --interactive` is what I use, and I think that's what most
people use.

This allows you to easily edit commits and add specific changes to
specific commits.

Once you are familiar with this process it's easier to understand fixup
commits, but I'd say rebasing comes first.

Cheers.

-- 
Felipe Contreras

[PATCH v3 2/9] ssh signing: add documentation

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:18

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 Documentation/config/gpg.txt  | 35 +++++++++++++++++++++++++++++++++--
 Documentation/config/user.txt |  6 ++++++
 2 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..16af0b0ada8 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -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.
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index 59aec7c3aed..b3c2f2c541e 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -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.
-- 
gitgitgadget

[PATCH v3 1/9] Add commit, tag & push signing via SSH keys

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:20

From: Fabian Stelzer <redacted>

Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.

Start with adding the new signature format, new config options and
rename some fields for consistency.

This feature makes git signing much more accessible to the average user.
Usually they have a SSH Key for pushing code already. Using it
for signing commits allows us to verify not only the transport but the
pushed code as well.

In our corporate environemnt we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which i think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.

Signed-off-by: Fabian Stelzer <redacted>
---
 fmt-merge-msg.c |   4 +-
 gpg-interface.c | 122 +++++++++++++++++++++++++++++++++---------------
 gpg-interface.h |   5 +-
 log-tree.c      |   8 ++--
 pretty.c        |   4 +-
 5 files changed, 95 insertions(+), 48 deletions(-)
diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c
index 0f66818e0f8..1d7b64fa021 100644
--- a/fmt-merge-msg.c
+++ b/fmt-merge-msg.c
@@ -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);
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..3c9a48c8e7e 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -8,6 +8,7 @@
 #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 {
@@ -35,6 +36,14 @@ 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,
@@ -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);
 }
 
-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)
 {
 	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;
 
 	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);
 	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;
 }
@@ -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);
+	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);
+	}
+	if (status && !sigc->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);
@@ -388,12 +399,32 @@ 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);
@@ -425,6 +456,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);
@@ -437,7 +471,19 @@ 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)
diff --git a/gpg-interface.h b/gpg-interface.h
index 80567e48948..5dfd92b81f6 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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);
diff --git a/log-tree.c b/log-tree.c
index 7b823786c2c..20af9bd1c82 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -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);
diff --git a/pretty.c b/pretty.c
index b1ecd039cef..daa71394efd 100644
--- a/pretty.c
+++ b/pretty.c
@@ -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) {
-- 
gitgitgadget

[PATCH v3 0/9] RFC: Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:21

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

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 [off-list ref]
      
       ## 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 [off-list ref]
      
     - ## 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 hasnokey@nowhere.com &&
     -+	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 [off-list ref] 1112912653 -0700
     -+# 	Qcommitter C O Mitter [off-list ref] 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 [off-list ref] 1112912653 -0700
     ++	Qcommitter C O Mitter [off-list ref] 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

-- 
gitgitgadget

[PATCH v3 3/9] ssh signing: retrieve a default key from ssh-agent

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:22

From: Fabian Stelzer <redacted>

calls ssh-add -L and uses the first key

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
diff --git a/gpg-interface.c b/gpg-interface.c
index 3c9a48c8e7e..c956ed87475 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
-- 
gitgitgadget

[PATCH v3 5/9] ssh signing: provide a textual representation of the signing key

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:25

From: Fabian Stelzer <redacted>

for ssh the key can be a filename/path or even a literal ssh pubkey
in push certs and textual output we prefer the ssh fingerprint instead

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++
 gpg-interface.h |  7 +++++++
 send-pack.c     |  8 ++++----
 3 files changed, 58 insertions(+), 4 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index fa32a57d372..328af86c272 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
diff --git a/gpg-interface.h b/gpg-interface.h
index 5dfd92b81f6..1e842188c26 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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,
diff --git a/send-pack.c b/send-pack.c
index 9cb9f716509..191fc6da544 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -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;
 }
-- 
gitgitgadget

[PATCH v3 4/9] ssh signing: sign using either gpg or ssh keys

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:25

From: Fabian Stelzer <redacted>

implements the actual ssh-keygen -Y sign operation

Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.
If no signingkey is set then git will call 'ssh-add -L' to check for
available agent keys and use the first one for signing.

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 86 +++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 76 insertions(+), 10 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index c956ed87475..fa32a57d372 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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"));
 
-- 
gitgitgadget

[PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:27

From: Fabian Stelzer <redacted>

Verification uses the gpg.ssh.keyring file (see ssh-keygen(1) "ALLOWED
SIGNERS") which contains valid public keys and a principal (usually
user@domain). Depending on the environment this file can be managed by
the individual developer or for example generated by the central
repository server from known ssh keys with push access. If the
repository only allows signed commits / pushes then the file can even be
stored inside it.

To revoke a key put the public key without the principal prefix into
gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)
"KEY REVOCATION LISTS"). The same considerations about who to trust for
verification as with the keyring file apply.

Using SSH CA Keys with these files is also possible. Add
"cert-authority" as key option between the principal and the key to mark
it as a CA and all keys signed by it as valid for this CA.

Signed-off-by: Fabian Stelzer <redacted>
---
 builtin/receive-pack.c |   2 +
 gpg-interface.c        | 139 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 141 insertions(+)
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a34742513ac..62b11c5f3a4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 {
 	int status = parse_hide_refs_config(var, value, "receive");
 
+	git_gpg_config(var, value, NULL);
+
 	if (status)
 		return status;
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 328af86c272..1be88b87d96 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -3,6 +3,7 @@
 #include "config.h"
 #include "run-command.h"
 #include "strbuf.h"
+#include "dir.h"
 #include "gpg-interface.h"
 #include "sigchain.h"
 #include "tempfile.h"
@@ -156,6 +157,42 @@ static int parse_gpg_trust_level(const char *level,
 	return 1;
 }
 
+static void parse_ssh_output(struct signature_check *sigc)
+{
+	struct string_list parts = STRING_LIST_INIT_DUP;
+	char *line = 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
+	 */
+	sigc->result = 'B';
+	sigc->trust_level = TRUST_NEVER;
+
+	line = xmemdupz(sigc->output, strcspn(sigc->output, "\n"));
+	string_list_split(&parts, line, ' ', 8);
+	if (parts.nr >= 9 && starts_with(line, "Good \"git\" signature for ")) {
+		/* Valid signature for a trusted signer */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_FULLY;
+		sigc->signer = xstrdup(parts.items[4].string);
+		sigc->fingerprint = xstrdup(parts.items[8].string);
+		sigc->key = xstrdup(sigc->fingerprint);
+	} else if (parts.nr >= 7 && starts_with(line, "Good \"git\" signature with ")) {
+		/* Valid signature, but key unknown */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_UNDEFINED;
+		sigc->fingerprint = xstrdup(parts.items[6].string);
+		sigc->key = xstrdup(sigc->fingerprint);
+	}
+	trace_printf("trace: sigc result %c/%d - %s %s %s", sigc->result, sigc->trust_level, sigc->signer, sigc->fingerprint, sigc->key);
+
+	string_list_clear(&parts, 0);
+	FREE_AND_NULL(line);
+}
+
 static void parse_gpg_output(struct signature_check *sigc)
 {
 	const char *buf = sigc->gpg_status;
@@ -269,6 +306,108 @@ 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)
+{
+	struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+	struct tempfile *temp;
+	int ret;
+	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)
+		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;
+	}
+
+	/* Find the principal from the signers */
+	strvec_pushl(&ssh_keygen.args,  fmt->program,
+					"-Y", "find-principals",
+					"-f", get_ssh_allowed_signers(),
+					"-s", temp->filename.buf,
+					NULL);
+	ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+	if (strstr(ssh_keygen_err.buf, "usage:")) {
+		error(_("openssh version > 8.2p1 is needed for ssh signature verification (ssh-keygen needs -Y find-principals/verify option)"));
+	}
+	if (ret || !ssh_keygen_out.len) {
+		/* We did not find a matching principal in the keyring - Check without validation */
+		child_process_init(&ssh_keygen);
+		strvec_pushl(&ssh_keygen.args,  fmt->program,
+						"-Y", "check-novalidate",
+						"-n", "git",
+						"-s", temp->filename.buf,
+						NULL);
+		ret = pipe_command(&ssh_keygen, payload, payload_size, &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+	} else {
+		/* Check every principal we found (one per line) */
+		for (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\n')) {
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, " \n");
+			principal = xmemdupz(line, trust_size);
+
+			child_process_init(&ssh_keygen);
+			strbuf_release(&ssh_keygen_out);
+			strbuf_release(&ssh_keygen_err);
+			strvec_push(&ssh_keygen.args,fmt->program);
+			/* We found principals - Try with each until we find a match */
+			strvec_pushl(&ssh_keygen.args,  "-Y", "verify",
+							"-n", "git",
+							"-f", get_ssh_allowed_signers(),
+							"-I", principal,
+							"-s", temp->filename.buf,
+							NULL);
+
+			if (ssh_revocation_file) {
+				if (file_exists(ssh_revocation_file)) {
+					strvec_pushl(&ssh_keygen.args, "-r", ssh_revocation_file, NULL);
+				} else {
+					warning(_("ssh signing revocation file configured but not found: %s"), ssh_revocation_file);
+				}
+			}
+
+			sigchain_push(SIGPIPE, SIG_IGN);
+			ret = pipe_command(&ssh_keygen, payload, payload_size,
+					&ssh_keygen_out, 0, &ssh_keygen_err, 0);
+			sigchain_pop(SIGPIPE);
+
+			ret &= starts_with(ssh_keygen_out.buf, "Good");
+			if (ret == 0)
+				break;
+		}
+	}
+
+	sigc->payload = xmemdupz(payload, payload_size);
+	strbuf_stripspace(&ssh_keygen_out, 0);
+	strbuf_stripspace(&ssh_keygen_err, 0);
+	strbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);
+	sigc->output = strbuf_detach(&ssh_keygen_out, NULL);
+	sigc->gpg_status = xstrdup(sigc->output);
+
+	parse_ssh_output(sigc);
+
+	delete_tempfile(&temp);
+	strbuf_release(&ssh_keygen_out);
+	strbuf_release(&ssh_keygen_err);
+
+	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)
-- 
gitgitgadget

[PATCH v3 7/9] ssh signing: add test prereqs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:28

From: Fabian Stelzer <redacted>

generate some ssh keys and a allowed keys keyring for testing

Signed-off-by: Fabian Stelzer <redacted>
---
 t/lib-gpg.sh | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index 9fc5241228e..c65cdde9e5f 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -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;
-- 
gitgitgadget

[PATCH v3 8/9] ssh signing: duplicate t7510 tests for commits

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:29

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++
 1 file changed, 398 insertions(+)
 create mode 100755 t/t7527-signed-commit-ssh.sh
diff --git a/t/t7527-signed-commit-ssh.sh b/t/t7527-signed-commit-ssh.sh
new file mode 100755
index 00000000000..305b3b9160b
--- /dev/null
+++ b/t/t7527-signed-commit-ssh.sh
@@ -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 <author@example.com> 1112912653 -0700
+	Qcommitter C O Mitter <committer@example.com> 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
-- 
gitgitgadget

[PATCH v3 9/9] ssh signing: add more tests for logs, tags & push certs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-14 12:10:31

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 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
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 350cfa35936..41767627ad0 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -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 &&
diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh
index bba768f5ded..37c97756032 100755
--- a/t/t5534-push-signed.sh
+++ b/t/t5534-push-signed.sh
@@ -137,6 +137,53 @@ 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 &&
@@ -276,6 +323,60 @@ 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 hasnokey@nowhere.com &&
+	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 &&
diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh
new file mode 100755
index 00000000000..2148a246385
--- /dev/null
+++ b/t/t7031-verify-tag-signed-ssh.sh
@@ -0,0 +1,161 @@
+#!/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}" &&
+	(
+		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 "${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 --raw $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 --raw $tag 2>actual &&
+			grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			! grep "${BAD_SIGNATURE}" actual &&
+			echo $tag OK || exit 1
+		done
+	)
+'
+
+test_expect_success GPGSSH 'verify signatures with --raw ssh' '
+	test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
+	git verify-tag --raw sixth-signed 2>actual &&
+	grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
+	! grep "${BAD_SIGNATURE}" actual &&
+	echo sixth-signed OK
+'
+
+test_expect_success GPGSSH 'verify multiple tags ssh' '
+	test_config gpg.ssh.keyring "${SIGNING_KEYRING}" &&
+	tags="seventh-signed sixth-signed" &&
+	for i in $tags
+	do
+		git verify-tag -v --raw $i || return 1
+	done >expect.stdout 2>expect.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <expect.stderr.1 >expect.stderr &&
+	git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <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 - ssh' '
+	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 - ssh' '
+	test_must_fail git verify-tag --format="tagname : %(tag)" $(cat forged1.tag) >actual-forged &&
+	test_must_be_empty actual-forged
+'
+
+test_done
-- 
gitgitgadget

Re: [PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures

From: Gwyneth Morgan <hidden>
Date: 2021-07-16 00:16:44

On 2021-07-14 12:10:10+0000, Fabian Stelzer via GitGitGadget wrote:
+		for (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\n')) {
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, " \n");
+			principal = xmemdupz(line, trust_size);
This breaks on principals with spaces in them (principals in the allowed
signers file can have spaces if surrounded by quotes). Looks like
strcspn should reject "\n" instead of " \n".

BTW, thanks for working on this feature. It seems much more convenient
than GPG in my testing.

Re: [PATCH v3 6/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer <hidden>
Date: 2021-07-16 07:00:34

On 16.07.21 02:07, Gwyneth Morgan wrote:
On 2021-07-14 12:10:10+0000, Fabian Stelzer via GitGitGadget wrote:
quoted
+		for (line = ssh_keygen_out.buf; *line; line = strchrnul(line + 1, '\n')) {
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, " \n");
+			principal = xmemdupz(line, trust_size);
This breaks on principals with spaces in them (principals in the allowed
signers file can have spaces if surrounded by quotes). Looks like
strcspn should reject "\n" instead of " \n".

BTW, thanks for working on this feature. It seems much more convenient
than GPG in my testing.
Oh thanks. Very nice catch. Easily fixed here but i'll have to rewrite 
the verification output parsing to account for this as well.
I will add a testcase too.

[PATCH v4 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:23

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

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 [off-list ref]
      
       ## 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 [off-list ref]
      
       ## 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 [off-list ref]
      
       ## 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 [off-list ref]
      
       ## 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 [off-list ref]
      
     @@ 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 [off-list ref]
      
     @@ 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 [off-list ref]
      
     @@ 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.: user1@example.com,user2@example.com 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

-- 
gitgitgadget

[PATCH v4 1/9] ssh signing: preliminary refactoring and clean-up

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:25

From: Fabian Stelzer <redacted>

Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.

In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.

To be able to implement new signing formats this commit:
 - makes the sigc structure more generic by renaming "gpg_output" to
   "output"
 - introduces function pointers in the gpg_format structure to call
   format specific signing and verification functions
 - moves format detection from verify_signed_buffer into the check_signature
   api function and calls the format specific verify
 - renames and wraps sign_buffer to handle format specific signing logic
   as well

Signed-off-by: Fabian Stelzer <redacted>
---
 fmt-merge-msg.c |   6 +--
 gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
 gpg-interface.h |   2 +-
 log-tree.c      |   8 ++--
 pretty.c        |   4 +-
 5 files changed, 74 insertions(+), 50 deletions(-)
diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c
index 0f66818e0f8..fb300bb4b67 100644
--- a/fmt-merge-msg.c
+++ b/fmt-merge-msg.c
@@ -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);
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..31cf4ba3938 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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);
 };
 
 static const char *openpgp_verify_args[] = {
@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {
 	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,
-	  .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,
 	},
 };
 
@@ -72,7 +93,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 +278,16 @@ 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_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_stdout = STRBUF_INIT;
+	struct strbuf gpg_stderr = STRBUF_INIT;
 
 	temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
 	if (!temp)
@@ -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);
-
 	strvec_push(&gpg.args, fmt->program);
 	strvec_pushv(&gpg.args, fmt->verify_args);
 	strvec_pushl(&gpg.args,
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,
 		     "--verify", temp->filename.buf, "-",
 		     NULL);
 
-	if (!gpg_status)
-		gpg_status = &buf;
-
 	sigchain_push(SIGPIPE, SIG_IGN);
-	ret = pipe_command(&gpg, payload, payload_size,
-			   gpg_status, 0, gpg_output, 0);
+	ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,
+			   &gpg_stderr, 0);
 	sigchain_pop(SIGPIPE);
 
 	delete_tempfile(&temp);
 
-	ret |= !strstr(gpg_status->buf, "\n[GNUPG:] GOODSIG ");
-	strbuf_release(&buf); /* no matter it was used or not */
+	ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
+	sigc->payload = xmemdupz(payload, payload_size);
+	sigc->output = strbuf_detach(&gpg_stderr, NULL);
+	sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);
+
+	parse_gpg_output(sigc);
+
+	strbuf_release(&gpg_stdout);
+	strbuf_release(&gpg_stderr);
 
 	return ret;
 }
@@ -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_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);
+
+	status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
+					   slen);
+
+	if (status && !sigc->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;
+	const char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :
+							    sigc->output;
 
 	if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
 		fputs(sigc->payload, stdout);
@@ -441,6 +459,12 @@ const char *get_signing_key(void)
 }
 
 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;
diff --git a/gpg-interface.h b/gpg-interface.h
index 80567e48948..feac4decf8b 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -17,7 +17,7 @@ enum signature_trust_level {
 
 struct signature_check {
 	char *payload;
-	char *gpg_output;
+	char *output;
 	char *gpg_status;
 
 	/*
diff --git a/log-tree.c b/log-tree.c
index 7b823786c2c..20af9bd1c82 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -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);
diff --git a/pretty.c b/pretty.c
index b1ecd039cef..daa71394efd 100644
--- a/pretty.c
+++ b/pretty.c
@@ -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) {
-- 
gitgitgadget

[PATCH v4 2/9] ssh signing: add ssh signature format and signing using ssh keys

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:30

From: Fabian Stelzer <redacted>

implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function

Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.

gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 129 insertions(+), 8 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index 31cf4ba3938..a086123754d 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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 *sig
 	return 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;
+}
-- 
gitgitgadget

[PATCH v4 3/9] ssh signing: retrieve a default key from ssh-agent

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:31

From: Fabian Stelzer <redacted>

if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index a086123754d..35e584b94ef 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
-- 
gitgitgadget

[PATCH v4 4/9] ssh signing: provide a textual representation of the signing key

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:32

From: Fabian Stelzer <redacted>

for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.
in push certs and textual output we prefer the ssh fingerprint instead.

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 gpg-interface.h |  6 ++++++
 send-pack.c     |  8 ++++----
 3 files changed, 56 insertions(+), 4 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index 35e584b94ef..2c6eaf47d0f 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
diff --git a/gpg-interface.h b/gpg-interface.h
index feac4decf8b..beefacbb1e9 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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);
diff --git a/send-pack.c b/send-pack.c
index 9cb9f716509..191fc6da544 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -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;
 }
-- 
gitgitgadget

[PATCH v4 5/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:33

From: Fabian Stelzer <redacted>

to verify a ssh signature we first call ssh-keygen -Y find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers identity.

Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED
SIGNERS") which contains valid public keys and a principal (usually
user@domain). Depending on the environment this file can be managed by
the individual developer or for example generated by the central
repository server from known ssh keys with push access. If the
repository only allows signed commits / pushes then the file can even be
stored inside it.

To revoke a key put the public key without the principal prefix into
gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)
"KEY REVOCATION LISTS"). The same considerations about who to trust for
verification as with the allowedSignersFile apply.

Using SSH CA Keys with these files is also possible. Add
"cert-authority" as key option between the principal and the key to mark
it as a CA and all keys signed by it as valid for this CA.

Signed-off-by: Fabian Stelzer <redacted>
---
 builtin/receive-pack.c |   2 +
 gpg-interface.c        | 174 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 175 insertions(+), 1 deletion(-)
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a34742513ac..62b11c5f3a4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 {
 	int status = parse_hide_refs_config(var, value, "receive");
 
+	git_gpg_config(var, value, NULL);
+
 	if (status)
 		return status;
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 2c6eaf47d0f..761aa91d648 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -3,11 +3,13 @@
 #include "config.h"
 #include "run-command.h"
 #include "strbuf.h"
+#include "dir.h"
 #include "gpg-interface.h"
 #include "sigchain.h"
 #include "tempfile.h"
 
 static char *configured_signing_key;
+static const char *ssh_allowed_signers, *ssh_revocation_file;
 static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
 
 struct gpg_format {
@@ -51,6 +53,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
 				    struct gpg_format *fmt, const char *payload,
 				    size_t payload_size, const char *signature,
 				    size_t signature_size);
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
+				    struct gpg_format *fmt, const char *payload,
+				    size_t payload_size, const char *signature,
+				    size_t signature_size);
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 			   const char *signing_key);
 static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
@@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = {
 		.program = "ssh-keygen",
 		.verify_args = ssh_verify_args,
 		.sigs = ssh_sigs,
-		.verify_signed_buffer = NULL, /* TODO */
+		.verify_signed_buffer = verify_ssh_signed_buffer,
 		.sign_buffer = sign_buffer_ssh
 	},
 };
@@ -343,6 +349,160 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
 	return ret;
 }
 
+static void parse_ssh_output(struct signature_check *sigc)
+{
+	const char *line, *principal, *search;
+
+	/*
+	 * ssh-keysign output should be:
+	 * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
+	 * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT
+	 * or for valid but unknown keys:
+	 * Good "git" signature with RSA key SHA256:FINGERPRINT
+	 */
+	sigc->result = 'B';
+	sigc->trust_level = TRUST_NEVER;
+
+	line = xmemdupz(sigc->output, strcspn(sigc->output, "\n"));
+
+	if (skip_prefix(line, "Good \"git\" signature for ", &line)) {
+		/* Valid signature and known principal */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_FULLY;
+
+		/* Search for the last "with" to get the full principal */
+		principal = line;
+		do {
+			search = strstr(line, " with ");
+			if (search)
+				line = search + 1;
+		} while (search != NULL);
+		sigc->signer = xmemdupz(principal, line - principal - 1);
+		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+		sigc->key = xstrdup(sigc->fingerprint);
+	} else if (skip_prefix(line, "Good \"git\" signature with ", &line)) {
+		/* Valid signature, but key unknown */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_UNDEFINED;
+		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+		sigc->key = xstrdup(sigc->fingerprint);
+	}
+}
+
+static 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;
+	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)
+		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;
+	}
+
+	/* Find the principal from the signers */
+	strvec_pushl(&ssh_keygen.args, fmt->program, "-Y", "find-principals",
+		     "-f", get_ssh_allowed_signers(), "-s", temp->filename.buf,
+		     NULL);
+	ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,
+			   &ssh_keygen_err, 0);
+	if (ret && strstr(ssh_keygen_err.buf, "usage:")) {
+		error(_("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)"));
+		return ret;
+	}
+	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);
+		ret = pipe_command(&ssh_keygen, payload, payload_size,
+				   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+	} else {
+		/* Check every principal we found (one per line) */
+		for (line = ssh_keygen_out.buf; *line;
+		     line = strchrnul(line + 1, '\n')) {
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, "\n");
+			principal = xmemdupz(line, trust_size);
+
+			child_process_init(&ssh_keygen);
+			strbuf_release(&ssh_keygen_out);
+			strbuf_release(&ssh_keygen_err);
+			strvec_push(&ssh_keygen.args, fmt->program);
+			/* We found principals - Try with each until we find a
+			 * match */
+			strvec_pushl(&ssh_keygen.args, "-Y", "verify", "-n",
+				     "git", "-f", get_ssh_allowed_signers(),
+				     "-I", principal, "-s", temp->filename.buf,
+				     NULL);
+
+			if (ssh_revocation_file) {
+				if (file_exists(ssh_revocation_file)) {
+					strvec_pushl(&ssh_keygen.args, "-r",
+						     ssh_revocation_file, NULL);
+				} else {
+					warning(_("ssh signing revocation file configured but not found: %s"),
+						ssh_revocation_file);
+				}
+			}
+
+			sigchain_push(SIGPIPE, SIG_IGN);
+			ret = pipe_command(&ssh_keygen, payload, payload_size,
+					   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+			sigchain_pop(SIGPIPE);
+
+			FREE_AND_NULL(principal);
+
+			ret &= starts_with(ssh_keygen_out.buf, "Good");
+			if (ret == 0)
+				break;
+		}
+	}
+
+	sigc->payload = xmemdupz(payload, payload_size);
+	strbuf_stripspace(&ssh_keygen_out, 0);
+	strbuf_stripspace(&ssh_keygen_err, 0);
+	strbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);
+	sigc->output = strbuf_detach(&ssh_keygen_out, NULL);
+	sigc->gpg_status = xstrdup(sigc->output);
+
+	parse_ssh_output(sigc);
+
+	delete_tempfile(&temp);
+	strbuf_release(&ssh_keygen_out);
+	strbuf_release(&ssh_keygen_err);
+
+	return ret;
+}
+
 int check_signature(const char *payload, size_t plen, const char *signature,
 	size_t slen, struct signature_check *sigc)
 {
@@ -453,6 +613,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (!strcmp(var, "gpg.ssh.allowedsignersfile")) {
+		if (!value)
+			return config_error_nonbool(var);
+		return git_config_string(&ssh_allowed_signers, var, value);
+	}
+
+	if (!strcmp(var, "gpg.ssh.revocationFile")) {
+		if (!value)
+			return config_error_nonbool(var);
+		return git_config_string(&ssh_revocation_file, var, value);
+	}
+
 	if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
 		fmtname = "openpgp";
 
-- 
gitgitgadget

[PATCH v4 6/9] ssh signing: add test prereqs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:34

From: Fabian Stelzer <redacted>

generate some ssh keys and a allowedSignersFile for testing

Signed-off-by: Fabian Stelzer <redacted>
---
 t/lib-gpg.sh | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index 9fc5241228e..b4fbcad4bf3 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -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;
-- 
gitgitgadget

[PATCH v4 7/9] ssh signing: duplicate t7510 tests for commits

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:35

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 t/t7527-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++
 1 file changed, 398 insertions(+)
 create mode 100755 t/t7527-signed-commit-ssh.sh
diff --git a/t/t7527-signed-commit-ssh.sh b/t/t7527-signed-commit-ssh.sh
new file mode 100755
index 00000000000..e2c48f69e6d
--- /dev/null
+++ b/t/t7527-signed-commit-ssh.sh
@@ -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 <author@example.com> 1112912653 -0700
+	Qcommitter C O Mitter <committer@example.com> 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
-- 
gitgitgadget

[PATCH v4 8/9] ssh signing: add more tests for logs, tags & push certs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:36

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 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
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 39e746fbcbe..afd7f2516ee 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -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 &&
diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh
index bba768f5ded..d590249b995 100755
--- a/t/t5534-push-signed.sh
+++ b/t/t5534-push-signed.sh
@@ -137,6 +137,53 @@ 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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 with number 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 &&
@@ -276,6 +323,60 @@ 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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 hasnokey@nowhere.com &&
+	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 with number 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 &&
diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh
new file mode 100755
index 00000000000..05bf520a332
--- /dev/null
+++ b/t/t7031-verify-tag-signed-ssh.sh
@@ -0,0 +1,161 @@
+#!/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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	(
+		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.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 &&
+	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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	(
+		for tag in initial second merge fourth-signed sixth-signed seventh-signed
+		do
+			git verify-tag --raw $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 --raw $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 --raw $tag 2>actual &&
+			grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			! grep "${BAD_SIGNATURE}" actual &&
+			echo $tag OK || exit 1
+		done
+	)
+'
+
+test_expect_success GPGSSH 'verify signatures with --raw ssh' '
+	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 &&
+	echo sixth-signed OK
+'
+
+test_expect_success GPGSSH 'verify multiple tags ssh' '
+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	tags="seventh-signed sixth-signed" &&
+	for i in $tags
+	do
+		git verify-tag -v --raw $i || return 1
+	done >expect.stdout 2>expect.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <expect.stderr.1 >expect.stderr &&
+	git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <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 - ssh' '
+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 - ssh' '
+	test_must_fail git verify-tag --format="tagname : %(tag)" $(cat forged1.tag) >actual-forged &&
+	test_must_be_empty actual-forged
+'
+
+test_done
-- 
gitgitgadget

[PATCH v4 9/9] ssh signing: add documentation

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-19 13:33:37

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 Documentation/config/gpg.txt  | 39 +++++++++++++++++++++++++++++++++--
 Documentation/config/user.txt |  6 ++++++
 2 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..dc790512e86 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -11,13 +11,13 @@ gpg.program::
 
 gpg.format::
 	Specifies which key format to use when signing with `--gpg-sign`.
-	Default is "openpgp" and another possible value is "x509".
+	Default is "openpgp". Other possible values are "x509", "ssh".
 
 gpg.<format>.program::
 	Use this to customize the program used for the signing format you
 	chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
 	be used as a legacy synonym for `gpg.openpgp.program`. The default
-	value for `gpg.x509.program` is "gpgsm".
+	value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
 
 gpg.minTrustLevel::
 	Specifies a minimum trust level for signature verification.  If
@@ -33,3 +33,38 @@ gpg.minTrustLevel::
 * `marginal`
 * `fully`
 * `ultimate`
+
+gpg.ssh.allowedSignersFile::
+	A file containing ssh public keys which you are willing to trust.
+	The file consists of one or more lines of principals followed by an ssh
+	public key.
+	e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...
+	See ssh-keygen(1) "ALLOWED SIGNERS" for details.
+	The principal is only used to identify the key and is available when
+	verifying a signature.
++
+SSH has no concept of trust levels like gpg does. To be able to differentiate
+between valid signatures and trusted signatures the trust level of a signature
+verification is set to `fully` when the public key is present in the allowedSignersFile.
+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.
+Otherwise valid but untrusted signatures will still verify but show no principal
+name of the signer.
++
+This file can be set to a location outside of the repository and every developer
+maintains their own trust store. A central repository server could generate this
+file automatically from ssh keys with push access to verify the code against.
+In a corporate setting this file is probably generated at a global location
+from automation that already handles developer ssh keys.
++
+A repository that only allows signed commits can store the file
+in the repository itself using a path relative to the top-level of the working tree.
+This way only committers with an already valid key can add or change keys in the keyring.
++
+Using a SSH CA key with the cert-authority option
+(see ssh-keygen(1) "CERTIFICATES") is also valid.
+
+gpg.ssh.revocationFile::
+	Either a SSH KRL or a list of revoked public keys (without the principal prefix).
+	See ssh-keygen(1) for details.
+	If a public key is found in this file then it will always be treated
+	as having trust level "never" and signatures will show as invalid.
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index 59aec7c3aed..b3c2f2c541e 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -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.
-- 
gitgitgadget

[PATCH v5 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:15:50

I have added support for using keyfiles directly, lots of tests and
generally cleaned up the signing & verification code a lot.

I can still rename things from being gpg specific to a more general
"signing" but thats rather cosmetic. Also i'm not sure if i named the new
test files correctly.

openssh 8.7 will add valid-after, valid-before options to the allowed keys
keyring. This allows us to pass the commit timestamp to the verification
call and make key rollover possible and still be able to verify older
commits. Set valid-after=NOW when adding your key to the keyring and set
valid-before to make it fail if used after a certain date. Software like
gitolite/github or corporate automation can do this automatically when ssh
push keys are addded / removed I will add this feature in a follow up patch
afterwards.

v3 addresses some issues & refactoring and splits the large commit into
several smaller ones.

v4:

 * restructures and cleans up the whole patch set - patches build on its own
   now and commit messages try to explain whats going on
 * got rid of the if branches and used callback functions in the format
   struct
 * fixed a bug with whitespace in principal identifiers that required a
   rewrite of the parse_ssh_output function
 * rewrote documentation to be more clear - also renamed keyring back to
   allowedSignersFile

v5:

 * moved t7527 to t7528 to not collide with another patch in "seen"
 * clean up return logic for failed signing & verification
 * some minor renames / reformatting to make things clearer

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
      
          Signed-off-by: Fabian Stelzer [off-list ref]
      
     - ## 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

-- 
gitgitgadget

[PATCH v5 1/9] ssh signing: preliminary refactoring and clean-up

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:15:50

From: Fabian Stelzer <redacted>

Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.

In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.

To be able to implement new signing formats this commit:
 - makes the sigc structure more generic by renaming "gpg_output" to
   "output"
 - introduces function pointers in the gpg_format structure to call
   format specific signing and verification functions
 - moves format detection from verify_signed_buffer into the check_signature
   api function and calls the format specific verify
 - renames and wraps sign_buffer to handle format specific signing logic
   as well

Signed-off-by: Fabian Stelzer <redacted>
---
 fmt-merge-msg.c |   6 +--
 gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
 gpg-interface.h |   2 +-
 log-tree.c      |   8 ++--
 pretty.c        |   4 +-
 5 files changed, 74 insertions(+), 50 deletions(-)
diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c
index 0f66818e0f8..fb300bb4b67 100644
--- a/fmt-merge-msg.c
+++ b/fmt-merge-msg.c
@@ -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);
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..31cf4ba3938 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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);
 };
 
 static const char *openpgp_verify_args[] = {
@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {
 	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,
-	  .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,
 	},
 };
 
@@ -72,7 +93,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 +278,16 @@ 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_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_stdout = STRBUF_INIT;
+	struct strbuf gpg_stderr = STRBUF_INIT;
 
 	temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
 	if (!temp)
@@ -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);
-
 	strvec_push(&gpg.args, fmt->program);
 	strvec_pushv(&gpg.args, fmt->verify_args);
 	strvec_pushl(&gpg.args,
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,
 		     "--verify", temp->filename.buf, "-",
 		     NULL);
 
-	if (!gpg_status)
-		gpg_status = &buf;
-
 	sigchain_push(SIGPIPE, SIG_IGN);
-	ret = pipe_command(&gpg, payload, payload_size,
-			   gpg_status, 0, gpg_output, 0);
+	ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,
+			   &gpg_stderr, 0);
 	sigchain_pop(SIGPIPE);
 
 	delete_tempfile(&temp);
 
-	ret |= !strstr(gpg_status->buf, "\n[GNUPG:] GOODSIG ");
-	strbuf_release(&buf); /* no matter it was used or not */
+	ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
+	sigc->payload = xmemdupz(payload, payload_size);
+	sigc->output = strbuf_detach(&gpg_stderr, NULL);
+	sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);
+
+	parse_gpg_output(sigc);
+
+	strbuf_release(&gpg_stdout);
+	strbuf_release(&gpg_stderr);
 
 	return ret;
 }
@@ -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_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);
+
+	status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
+					   slen);
+
+	if (status && !sigc->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;
+	const char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :
+							    sigc->output;
 
 	if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
 		fputs(sigc->payload, stdout);
@@ -441,6 +459,12 @@ const char *get_signing_key(void)
 }
 
 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;
diff --git a/gpg-interface.h b/gpg-interface.h
index 80567e48948..feac4decf8b 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -17,7 +17,7 @@ enum signature_trust_level {
 
 struct signature_check {
 	char *payload;
-	char *gpg_output;
+	char *output;
 	char *gpg_status;
 
 	/*
diff --git a/log-tree.c b/log-tree.c
index 7b823786c2c..20af9bd1c82 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -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);
diff --git a/pretty.c b/pretty.c
index b1ecd039cef..daa71394efd 100644
--- a/pretty.c
+++ b/pretty.c
@@ -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) {
-- 
gitgitgadget

[PATCH v5 2/9] ssh signing: add ssh signature format and signing using ssh keys

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:15:53

From: Fabian Stelzer <redacted>

implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function

Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.

gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 129 insertions(+), 8 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index 31cf4ba3938..c131977b347 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {
 	NULL
 };
 
+static const char *ssh_verify_args[] = { NULL };
+static const char *ssh_sigs[] = {
+	"-----BEGIN SSH SIGNATURE-----",
+	NULL
+};
+
 static int verify_gpg_signed_buffer(struct signature_check *sigc,
 				    struct gpg_format *fmt, const char *payload,
 				    size_t payload_size, const char *signature,
 				    size_t signature_size);
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 			   const char *signing_key);
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+			   const char *signing_key);
 
 static struct gpg_format gpg_format[] = {
 	{
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {
 		.verify_signed_buffer = verify_gpg_signed_buffer,
 		.sign_buffer = sign_buffer_gpg,
 	},
+	{
+		.name = "ssh",
+		.program = "ssh-keygen",
+		.verify_args = ssh_verify_args,
+		.sigs = ssh_sigs,
+		.verify_signed_buffer = NULL, /* TODO */
+		.sign_buffer = sign_buffer_ssh
+	},
 };
 
 static struct gpg_format *use_format = &gpg_format[0];
@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)
 	if (!strcmp(var, "gpg.x509.program"))
 		fmtname = "x509";
 
+	if (!strcmp(var, "gpg.ssh.program"))
+		fmtname = "ssh";
+
 	if (fmtname) {
 		fmt = get_format_by_name(fmtname);
 		return git_config_string(&fmt->program, var, value);
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
 	return use_format->sign_buffer(buffer, signature, signing_key);
 }
 
+/*
+ * Strip CR from the line endings, in case we are on Windows.
+ * NEEDSWORK: make it trim only CRs before LFs and rename
+ */
+static void remove_cr_after(struct strbuf *buffer, size_t offset)
+{
+	size_t i, j;
+
+	for (i = j = offset; i < buffer->len; i++) {
+		if (buffer->buf[i] != '\r') {
+			if (i != j)
+				buffer->buf[j] = buffer->buf[i];
+			j++;
+		}
+	}
+	strbuf_setlen(buffer, j);
+}
+
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 		    const char *signing_key)
 {
 	struct child_process gpg = CHILD_PROCESS_INIT;
 	int ret;
-	size_t i, j, bottom;
+	size_t bottom;
 	struct strbuf gpg_status = STRBUF_INIT;
 
 	strvec_pushl(&gpg.args,
@@ -494,13 +531,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;
+}
-- 
gitgitgadget

[PATCH v5 4/9] ssh signing: provide a textual representation of the signing key

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:15:57

From: Fabian Stelzer <redacted>

for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.
in push certs and textual output we prefer the ssh fingerprint instead.

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 gpg-interface.h |  6 ++++++
 send-pack.c     |  8 ++++----
 3 files changed, 56 insertions(+), 4 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index 3afacb48900..ec48a37b6cc 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
diff --git a/gpg-interface.h b/gpg-interface.h
index feac4decf8b..beefacbb1e9 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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);
diff --git a/send-pack.c b/send-pack.c
index 5a79e0e7110..50cca7e439b 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -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;
 }
-- 
gitgitgadget

[PATCH v5 3/9] ssh signing: retrieve a default key from ssh-agent

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:15:58

From: Fabian Stelzer <redacted>

if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index c131977b347..3afacb48900 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
-- 
gitgitgadget

[PATCH v5 5/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:16:00

From: Fabian Stelzer <redacted>

to verify a ssh signature we first call ssh-keygen -Y find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers identity.

Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED
SIGNERS") which contains valid public keys and a principal (usually
user@domain). Depending on the environment this file can be managed by
the individual developer or for example generated by the central
repository server from known ssh keys with push access. If the
repository only allows signed commits / pushes then the file can even be
stored inside it.

To revoke a key put the public key without the principal prefix into
gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)
"KEY REVOCATION LISTS"). The same considerations about who to trust for
verification as with the allowedSignersFile apply.

Using SSH CA Keys with these files is also possible. Add
"cert-authority" as key option between the principal and the key to mark
it as a CA and all keys signed by it as valid for this CA.

Signed-off-by: Fabian Stelzer <redacted>
---
 builtin/receive-pack.c |   2 +
 gpg-interface.c        | 179 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 180 insertions(+), 1 deletion(-)
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a34742513ac..62b11c5f3a4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 {
 	int status = parse_hide_refs_config(var, value, "receive");
 
+	git_gpg_config(var, value, NULL);
+
 	if (status)
 		return status;
 
diff --git a/gpg-interface.c b/gpg-interface.c
index ec48a37b6cc..703225c3cd3 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -3,11 +3,13 @@
 #include "config.h"
 #include "run-command.h"
 #include "strbuf.h"
+#include "dir.h"
 #include "gpg-interface.h"
 #include "sigchain.h"
 #include "tempfile.h"
 
 static char *configured_signing_key;
+static const char *ssh_allowed_signers, *ssh_revocation_file;
 static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
 
 struct gpg_format {
@@ -51,6 +53,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
 				    struct gpg_format *fmt, const char *payload,
 				    size_t payload_size, const char *signature,
 				    size_t signature_size);
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
+				    struct gpg_format *fmt, const char *payload,
+				    size_t payload_size, const char *signature,
+				    size_t signature_size);
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 			   const char *signing_key);
 static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
@@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = {
 		.program = "ssh-keygen",
 		.verify_args = ssh_verify_args,
 		.sigs = ssh_sigs,
-		.verify_signed_buffer = NULL, /* TODO */
+		.verify_signed_buffer = verify_ssh_signed_buffer,
 		.sign_buffer = sign_buffer_ssh
 	},
 };
@@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
 	return ret;
 }
 
+static void parse_ssh_output(struct signature_check *sigc)
+{
+	const char *line, *principal, *search;
+
+	/*
+	 * ssh-keysign output should be:
+	 * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
+	 * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT
+	 * or for valid but unknown keys:
+	 * Good "git" signature with RSA key SHA256:FINGERPRINT
+	 */
+	sigc->result = 'B';
+	sigc->trust_level = TRUST_NEVER;
+
+	line = xmemdupz(sigc->output, strcspn(sigc->output, "\n"));
+
+	if (skip_prefix(line, "Good \"git\" signature for ", &line)) {
+		/* Valid signature and known principal */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_FULLY;
+
+		/* Search for the last "with" to get the full principal */
+		principal = line;
+		do {
+			search = strstr(line, " with ");
+			if (search)
+				line = search + 1;
+		} while (search != NULL);
+		sigc->signer = xmemdupz(principal, line - principal - 1);
+		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+		sigc->key = xstrdup(sigc->fingerprint);
+	} else if (skip_prefix(line, "Good \"git\" signature with ", &line)) {
+		/* Valid signature, but key unknown */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_UNDEFINED;
+		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+		sigc->key = xstrdup(sigc->fingerprint);
+	}
+}
+
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
+				    struct gpg_format *fmt, const char *payload,
+				    size_t payload_size, const char *signature,
+				    size_t signature_size)
+{
+	struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+	struct tempfile *buffer_file;
+	int ret = -1;
+	const char *line;
+	size_t trust_size;
+	char *principal;
+	struct strbuf ssh_keygen_out = STRBUF_INIT;
+	struct strbuf ssh_keygen_err = STRBUF_INIT;
+
+	if (!ssh_allowed_signers) {
+		error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification"));
+		return -1;
+	}
+
+	buffer_file = mks_tempfile_t(".git_vtag_tmpXXXXXX");
+	if (!buffer_file)
+		return error_errno(_("could not create temporary file"));
+	if (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||
+	    close_tempfile_gently(buffer_file) < 0) {
+		error_errno(_("failed writing detached signature to '%s'"),
+			    buffer_file->filename.buf);
+		delete_tempfile(&buffer_file);
+		return -1;
+	}
+
+	/* Find the principal from the signers */
+	strvec_pushl(&ssh_keygen.args, fmt->program,
+		     "-Y", "find-principals",
+		     "-f", ssh_allowed_signers,
+		     "-s", buffer_file->filename.buf,
+		     NULL);
+	ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,
+			   &ssh_keygen_err, 0);
+	if (ret && strstr(ssh_keygen_err.buf, "usage:")) {
+		error(_("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)"));
+		goto out;
+	}
+	if (ret || !ssh_keygen_out.len) {
+		/* We did not find a matching principal in the allowedSigners - Check
+		 * without validation */
+		child_process_init(&ssh_keygen);
+		strvec_pushl(&ssh_keygen.args, fmt->program,
+			     "-Y", "check-novalidate",
+			     "-n", "git",
+			     "-s", buffer_file->filename.buf,
+			     NULL);
+		ret = pipe_command(&ssh_keygen, payload, payload_size,
+				   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+	} else {
+		/* Check every principal we found (one per line) */
+		for (line = ssh_keygen_out.buf; *line;
+		     line = strchrnul(line + 1, '\n')) {
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, "\n");
+			principal = xmemdupz(line, trust_size);
+
+			child_process_init(&ssh_keygen);
+			strbuf_release(&ssh_keygen_out);
+			strbuf_release(&ssh_keygen_err);
+			strvec_push(&ssh_keygen.args, fmt->program);
+			/* We found principals - Try with each until we find a
+			 * match */
+			strvec_pushl(&ssh_keygen.args, "-Y", "verify",
+				     "-n", "git",
+				     "-f", ssh_allowed_signers,
+				     "-I", principal,
+				     "-s", buffer_file->filename.buf,
+				     NULL);
+
+			if (ssh_revocation_file) {
+				if (file_exists(ssh_revocation_file)) {
+					strvec_pushl(&ssh_keygen.args, "-r",
+						     ssh_revocation_file, NULL);
+				} else {
+					warning(_("ssh signing revocation file configured but not found: %s"),
+						ssh_revocation_file);
+				}
+			}
+
+			sigchain_push(SIGPIPE, SIG_IGN);
+			ret = pipe_command(&ssh_keygen, payload, payload_size,
+					   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+			sigchain_pop(SIGPIPE);
+
+			FREE_AND_NULL(principal);
+
+			ret &= starts_with(ssh_keygen_out.buf, "Good");
+			if (ret == 0)
+				break;
+		}
+	}
+
+	sigc->payload = xmemdupz(payload, payload_size);
+	strbuf_stripspace(&ssh_keygen_out, 0);
+	strbuf_stripspace(&ssh_keygen_err, 0);
+	strbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);
+	sigc->output = strbuf_detach(&ssh_keygen_out, NULL);
+	sigc->gpg_status = xstrdup(sigc->output);
+
+	parse_ssh_output(sigc);
+
+out:
+	if (buffer_file)
+		delete_tempfile(&buffer_file);
+	strbuf_release(&ssh_keygen_out);
+	strbuf_release(&ssh_keygen_err);
+
+	return ret;
+}
+
 int check_signature(const char *payload, size_t plen, const char *signature,
 	size_t slen, struct signature_check *sigc)
 {
@@ -453,6 +618,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (!strcmp(var, "gpg.ssh.allowedsignersfile")) {
+		if (!value)
+			return config_error_nonbool(var);
+		return git_config_string(&ssh_allowed_signers, var, value);
+	}
+
+	if (!strcmp(var, "gpg.ssh.revocationFile")) {
+		if (!value)
+			return config_error_nonbool(var);
+		return git_config_string(&ssh_revocation_file, var, value);
+	}
+
 	if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
 		fmtname = "openpgp";
 
-- 
gitgitgadget

[PATCH v5 6/9] ssh signing: add test prereqs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:16:00

From: Fabian Stelzer <redacted>

generate some ssh keys and a allowedSignersFile for testing

Signed-off-by: Fabian Stelzer <redacted>
---
 t/lib-gpg.sh | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index 9fc5241228e..b4fbcad4bf3 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -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;
-- 
gitgitgadget

[PATCH v5 7/9] ssh signing: duplicate t7510 tests for commits

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:16:01

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++
 1 file changed, 398 insertions(+)
 create mode 100755 t/t7528-signed-commit-ssh.sh
diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh
new file mode 100755
index 00000000000..e2c48f69e6d
--- /dev/null
+++ b/t/t7528-signed-commit-ssh.sh
@@ -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 <author@example.com> 1112912653 -0700
+	Qcommitter C O Mitter <committer@example.com> 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
-- 
gitgitgadget

[PATCH v5 8/9] ssh signing: add more tests for logs, tags & push certs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:16:03

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 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
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 39e746fbcbe..afd7f2516ee 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -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 &&
diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh
index bba768f5ded..d590249b995 100755
--- a/t/t5534-push-signed.sh
+++ b/t/t5534-push-signed.sh
@@ -137,6 +137,53 @@ 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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 with number 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 &&
@@ -276,6 +323,60 @@ 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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 hasnokey@nowhere.com &&
+	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 with number 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 &&
diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh
new file mode 100755
index 00000000000..05bf520a332
--- /dev/null
+++ b/t/t7031-verify-tag-signed-ssh.sh
@@ -0,0 +1,161 @@
+#!/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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	(
+		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.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 &&
+	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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	(
+		for tag in initial second merge fourth-signed sixth-signed seventh-signed
+		do
+			git verify-tag --raw $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 --raw $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 --raw $tag 2>actual &&
+			grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			! grep "${BAD_SIGNATURE}" actual &&
+			echo $tag OK || exit 1
+		done
+	)
+'
+
+test_expect_success GPGSSH 'verify signatures with --raw ssh' '
+	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 &&
+	echo sixth-signed OK
+'
+
+test_expect_success GPGSSH 'verify multiple tags ssh' '
+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	tags="seventh-signed sixth-signed" &&
+	for i in $tags
+	do
+		git verify-tag -v --raw $i || return 1
+	done >expect.stdout 2>expect.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <expect.stderr.1 >expect.stderr &&
+	git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <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 - ssh' '
+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 - ssh' '
+	test_must_fail git verify-tag --format="tagname : %(tag)" $(cat forged1.tag) >actual-forged &&
+	test_must_be_empty actual-forged
+'
+
+test_done
-- 
gitgitgadget

[PATCH v5 9/9] ssh signing: add documentation

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-27 13:16:05

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 Documentation/config/gpg.txt  | 39 +++++++++++++++++++++++++++++++++--
 Documentation/config/user.txt |  6 ++++++
 2 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..dc790512e86 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -11,13 +11,13 @@ gpg.program::
 
 gpg.format::
 	Specifies which key format to use when signing with `--gpg-sign`.
-	Default is "openpgp" and another possible value is "x509".
+	Default is "openpgp". Other possible values are "x509", "ssh".
 
 gpg.<format>.program::
 	Use this to customize the program used for the signing format you
 	chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
 	be used as a legacy synonym for `gpg.openpgp.program`. The default
-	value for `gpg.x509.program` is "gpgsm".
+	value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
 
 gpg.minTrustLevel::
 	Specifies a minimum trust level for signature verification.  If
@@ -33,3 +33,38 @@ gpg.minTrustLevel::
 * `marginal`
 * `fully`
 * `ultimate`
+
+gpg.ssh.allowedSignersFile::
+	A file containing ssh public keys which you are willing to trust.
+	The file consists of one or more lines of principals followed by an ssh
+	public key.
+	e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...
+	See ssh-keygen(1) "ALLOWED SIGNERS" for details.
+	The principal is only used to identify the key and is available when
+	verifying a signature.
++
+SSH has no concept of trust levels like gpg does. To be able to differentiate
+between valid signatures and trusted signatures the trust level of a signature
+verification is set to `fully` when the public key is present in the allowedSignersFile.
+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.
+Otherwise valid but untrusted signatures will still verify but show no principal
+name of the signer.
++
+This file can be set to a location outside of the repository and every developer
+maintains their own trust store. A central repository server could generate this
+file automatically from ssh keys with push access to verify the code against.
+In a corporate setting this file is probably generated at a global location
+from automation that already handles developer ssh keys.
++
+A repository that only allows signed commits can store the file
+in the repository itself using a path relative to the top-level of the working tree.
+This way only committers with an already valid key can add or change keys in the keyring.
++
+Using a SSH CA key with the cert-authority option
+(see ssh-keygen(1) "CERTIFICATES") is also valid.
+
+gpg.ssh.revocationFile::
+	Either a SSH KRL or a list of revoked public keys (without the principal prefix).
+	See ssh-keygen(1) for details.
+	If a public key is found in this file then it will always be treated
+	as having trust level "never" and signatures will show as invalid.
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index 59aec7c3aed..b3c2f2c541e 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -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.
-- 
gitgitgadget

[PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:36:54

I have added support for using keyfiles directly, lots of tests and
generally cleaned up the signing & verification code a lot.

I can still rename things from being gpg specific to a more general
"signing" but thats rather cosmetic. Also i'm not sure if i named the new
test files correctly.

openssh 8.7 will add valid-after, valid-before options to the allowed keys
keyring. This allows us to pass the commit timestamp to the verification
call and make key rollover possible and still be able to verify older
commits. Set valid-after=NOW when adding your key to the keyring and set
valid-before to make it fail if used after a certain date. Software like
gitolite/github or corporate automation can do this automatically when ssh
push keys are addded / removed I will add this feature in a follow up patch
afterwards.

v3 addresses some issues & refactoring and splits the large commit into
several smaller ones.

v4:

 * restructures and cleans up the whole patch set - patches build on its own
   now and commit messages try to explain whats going on
 * got rid of the if branches and used callback functions in the format
   struct
 * fixed a bug with whitespace in principal identifiers that required a
   rewrite of the parse_ssh_output function
 * rewrote documentation to be more clear - also renamed keyring back to
   allowedSignersFile

v5:

 * moved t7527 to t7528 to not collide with another patch in "seen"
 * clean up return logic for failed signing & verification
 * some minor renames / reformatting to make things clearer

v6: fixed tests when using shm output dir

Fabian Stelzer (9):
  ssh signing: preliminary refactoring and clean-up
  ssh signing: add ssh signature format and signing using ssh keys
  ssh signing: retrieve a default key from ssh-agent
  ssh signing: provide a textual representation of the signing key
  ssh signing: parse ssh-keygen output and verify signatures
  ssh signing: add test prereqs
  ssh signing: duplicate t7510 tests for commits
  ssh signing: add more tests for logs, tags & push certs
  ssh signing: add documentation

 Documentation/config/gpg.txt     |  39 ++-
 Documentation/config/user.txt    |   6 +
 builtin/receive-pack.c           |   2 +
 fmt-merge-msg.c                  |   6 +-
 gpg-interface.c                  | 490 +++++++++++++++++++++++++++----
 gpg-interface.h                  |   8 +-
 log-tree.c                       |   8 +-
 pretty.c                         |   4 +-
 send-pack.c                      |   8 +-
 t/lib-gpg.sh                     |  29 ++
 t/t4202-log.sh                   |  23 ++
 t/t5534-push-signed.sh           | 101 +++++++
 t/t7031-verify-tag-signed-ssh.sh | 161 ++++++++++
 t/t7528-signed-commit-ssh.sh     | 398 +++++++++++++++++++++++++
 14 files changed, 1218 insertions(+), 65 deletions(-)
 create mode 100755 t/t7031-verify-tag-signed-ssh.sh
 create mode 100755 t/t7528-signed-commit-ssh.sh


base-commit: eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v6
Pull-Request: https://github.com/git/git/pull/1041

Range-diff vs v5:

  1:  7c8502c65b8 =  1:  7c8502c65b8 ssh signing: preliminary refactoring and clean-up
  2:  f05bab16096 =  2:  f05bab16096 ssh signing: add ssh signature format and signing using ssh keys
  3:  071e6173d8e =  3:  071e6173d8e ssh signing: retrieve a default key from ssh-agent
  4:  7d1d131ff5b =  4:  7d1d131ff5b ssh signing: provide a textual representation of the signing key
  5:  725764018ce =  5:  725764018ce ssh signing: parse ssh-keygen output and verify signatures
  6:  eb677b1b6a8 !  6:  18a26ca49e7 ssh signing: add test prereqs
     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
      +	test $? = 0 || exit 1;
      +	mkdir -p "${GNUPGHOME}" &&
      +	chmod 0700 "${GNUPGHOME}" &&
     -+	ssh-keygen -t ed25519 -N "" -f "${GNUPGHOME}/ed25519_ssh_signing_key" >/dev/null &&
     -+	ssh-keygen -t rsa -b 2048 -N "" -f "${GNUPGHOME}/rsa_2048_ssh_signing_key" >/dev/null &&
     -+	ssh-keygen -t ed25519 -N "super_secret" -f "${GNUPGHOME}/protected_ssh_signing_key" >/dev/null &&
     -+	find "${GNUPGHOME}" -name *ssh_signing_key.pub -exec cat {} \; | awk "{print \"\\\"principal with number \" NR \"\\\" \" \$0}" > "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
     ++	ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GNUPGHOME}/ed25519_ssh_signing_key" >/dev/null &&
     ++	echo "\"principal with number 1\" $(cat "${GNUPGHOME}/ed25519_ssh_signing_key.pub")" >> "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
     ++	ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GNUPGHOME}/rsa_2048_ssh_signing_key" >/dev/null &&
     ++	echo "\"principal with number 2\" $(cat "${GNUPGHOME}/rsa_2048_ssh_signing_key.pub")" >> "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
     ++	ssh-keygen -t ed25519 -N "super_secret" -C "git ed25519 encrypted key" -f "${GNUPGHOME}/protected_ssh_signing_key" >/dev/null &&
     ++	echo "\"principal with number 3\" $(cat "${GNUPGHOME}/protected_ssh_signing_key.pub")" >> "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
      +	cat "${GNUPGHOME}/ssh.all_valid.allowedSignersFile" &&
      +	ssh-keygen -t ed25519 -N "" -f "${GNUPGHOME}/untrusted_ssh_signing_key" >/dev/null
      +'
  7:  c877951df23 =  7:  01da9a07934 ssh signing: duplicate t7510 tests for commits
  8:  60265e8c399 =  8:  d9707443f5c ssh signing: add more tests for logs, tags & push certs
  9:  f758ce0ade4 =  9:  275af516eba ssh signing: add documentation

-- 
gitgitgadget

[PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:36:56

From: Fabian Stelzer <redacted>

Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.

In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.

To be able to implement new signing formats this commit:
 - makes the sigc structure more generic by renaming "gpg_output" to
   "output"
 - introduces function pointers in the gpg_format structure to call
   format specific signing and verification functions
 - moves format detection from verify_signed_buffer into the check_signature
   api function and calls the format specific verify
 - renames and wraps sign_buffer to handle format specific signing logic
   as well

Signed-off-by: Fabian Stelzer <redacted>
---
 fmt-merge-msg.c |   6 +--
 gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
 gpg-interface.h |   2 +-
 log-tree.c      |   8 ++--
 pretty.c        |   4 +-
 5 files changed, 74 insertions(+), 50 deletions(-)
diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c
index 0f66818e0f8..fb300bb4b67 100644
--- a/fmt-merge-msg.c
+++ b/fmt-merge-msg.c
@@ -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);
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..31cf4ba3938 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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);
 };
 
 static const char *openpgp_verify_args[] = {
@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {
 	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,
-	  .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,
 	},
 };
 
@@ -72,7 +93,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 +278,16 @@ 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_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_stdout = STRBUF_INIT;
+	struct strbuf gpg_stderr = STRBUF_INIT;
 
 	temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
 	if (!temp)
@@ -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);
-
 	strvec_push(&gpg.args, fmt->program);
 	strvec_pushv(&gpg.args, fmt->verify_args);
 	strvec_pushl(&gpg.args,
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,
 		     "--verify", temp->filename.buf, "-",
 		     NULL);
 
-	if (!gpg_status)
-		gpg_status = &buf;
-
 	sigchain_push(SIGPIPE, SIG_IGN);
-	ret = pipe_command(&gpg, payload, payload_size,
-			   gpg_status, 0, gpg_output, 0);
+	ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,
+			   &gpg_stderr, 0);
 	sigchain_pop(SIGPIPE);
 
 	delete_tempfile(&temp);
 
-	ret |= !strstr(gpg_status->buf, "\n[GNUPG:] GOODSIG ");
-	strbuf_release(&buf); /* no matter it was used or not */
+	ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
+	sigc->payload = xmemdupz(payload, payload_size);
+	sigc->output = strbuf_detach(&gpg_stderr, NULL);
+	sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);
+
+	parse_gpg_output(sigc);
+
+	strbuf_release(&gpg_stdout);
+	strbuf_release(&gpg_stderr);
 
 	return ret;
 }
@@ -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_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);
+
+	status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
+					   slen);
+
+	if (status && !sigc->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;
+	const char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :
+							    sigc->output;
 
 	if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
 		fputs(sigc->payload, stdout);
@@ -441,6 +459,12 @@ const char *get_signing_key(void)
 }
 
 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;
diff --git a/gpg-interface.h b/gpg-interface.h
index 80567e48948..feac4decf8b 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -17,7 +17,7 @@ enum signature_trust_level {
 
 struct signature_check {
 	char *payload;
-	char *gpg_output;
+	char *output;
 	char *gpg_status;
 
 	/*
diff --git a/log-tree.c b/log-tree.c
index 7b823786c2c..20af9bd1c82 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -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);
diff --git a/pretty.c b/pretty.c
index b1ecd039cef..daa71394efd 100644
--- a/pretty.c
+++ b/pretty.c
@@ -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) {
-- 
gitgitgadget

[PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:36:58

From: Fabian Stelzer <redacted>

implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function

Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.

gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 137 +++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 129 insertions(+), 8 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index 31cf4ba3938..c131977b347 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -41,12 +41,20 @@ static const char *x509_sigs[] = {
 	NULL
 };
 
+static const char *ssh_verify_args[] = { NULL };
+static const char *ssh_sigs[] = {
+	"-----BEGIN SSH SIGNATURE-----",
+	NULL
+};
+
 static int verify_gpg_signed_buffer(struct signature_check *sigc,
 				    struct gpg_format *fmt, const char *payload,
 				    size_t payload_size, const char *signature,
 				    size_t signature_size);
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 			   const char *signing_key);
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+			   const char *signing_key);
 
 static struct gpg_format gpg_format[] = {
 	{
@@ -65,6 +73,14 @@ static struct gpg_format gpg_format[] = {
 		.verify_signed_buffer = verify_gpg_signed_buffer,
 		.sign_buffer = sign_buffer_gpg,
 	},
+	{
+		.name = "ssh",
+		.program = "ssh-keygen",
+		.verify_args = ssh_verify_args,
+		.sigs = ssh_sigs,
+		.verify_signed_buffer = NULL, /* TODO */
+		.sign_buffer = sign_buffer_ssh
+	},
 };
 
 static struct gpg_format *use_format = &gpg_format[0];
@@ -443,6 +459,9 @@ int git_gpg_config(const char *var, const char *value, void *cb)
 	if (!strcmp(var, "gpg.x509.program"))
 		fmtname = "x509";
 
+	if (!strcmp(var, "gpg.ssh.program"))
+		fmtname = "ssh";
+
 	if (fmtname) {
 		fmt = get_format_by_name(fmtname);
 		return git_config_string(&fmt->program, var, value);
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
 	return use_format->sign_buffer(buffer, signature, signing_key);
 }
 
+/*
+ * Strip CR from the line endings, in case we are on Windows.
+ * NEEDSWORK: make it trim only CRs before LFs and rename
+ */
+static void remove_cr_after(struct strbuf *buffer, size_t offset)
+{
+	size_t i, j;
+
+	for (i = j = offset; i < buffer->len; i++) {
+		if (buffer->buf[i] != '\r') {
+			if (i != j)
+				buffer->buf[j] = buffer->buf[i];
+			j++;
+		}
+	}
+	strbuf_setlen(buffer, j);
+}
+
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 		    const char *signing_key)
 {
 	struct child_process gpg = CHILD_PROCESS_INIT;
 	int ret;
-	size_t i, j, bottom;
+	size_t bottom;
 	struct strbuf gpg_status = STRBUF_INIT;
 
 	strvec_pushl(&gpg.args,
@@ -494,13 +531,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;
+}
-- 
gitgitgadget

[PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:36:59

From: Fabian Stelzer <redacted>

if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index c131977b347..3afacb48900 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
-- 
gitgitgadget

[PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:37:00

From: Fabian Stelzer <redacted>

to verify a ssh signature we first call ssh-keygen -Y find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers identity.

Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED
SIGNERS") which contains valid public keys and a principal (usually
user@domain). Depending on the environment this file can be managed by
the individual developer or for example generated by the central
repository server from known ssh keys with push access. If the
repository only allows signed commits / pushes then the file can even be
stored inside it.

To revoke a key put the public key without the principal prefix into
gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)
"KEY REVOCATION LISTS"). The same considerations about who to trust for
verification as with the allowedSignersFile apply.

Using SSH CA Keys with these files is also possible. Add
"cert-authority" as key option between the principal and the key to mark
it as a CA and all keys signed by it as valid for this CA.

Signed-off-by: Fabian Stelzer <redacted>
---
 builtin/receive-pack.c |   2 +
 gpg-interface.c        | 179 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 180 insertions(+), 1 deletion(-)
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a34742513ac..62b11c5f3a4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 {
 	int status = parse_hide_refs_config(var, value, "receive");
 
+	git_gpg_config(var, value, NULL);
+
 	if (status)
 		return status;
 
diff --git a/gpg-interface.c b/gpg-interface.c
index ec48a37b6cc..703225c3cd3 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -3,11 +3,13 @@
 #include "config.h"
 #include "run-command.h"
 #include "strbuf.h"
+#include "dir.h"
 #include "gpg-interface.h"
 #include "sigchain.h"
 #include "tempfile.h"
 
 static char *configured_signing_key;
+static const char *ssh_allowed_signers, *ssh_revocation_file;
 static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
 
 struct gpg_format {
@@ -51,6 +53,10 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
 				    struct gpg_format *fmt, const char *payload,
 				    size_t payload_size, const char *signature,
 				    size_t signature_size);
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
+				    struct gpg_format *fmt, const char *payload,
+				    size_t payload_size, const char *signature,
+				    size_t signature_size);
 static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
 			   const char *signing_key);
 static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
@@ -78,7 +84,7 @@ static struct gpg_format gpg_format[] = {
 		.program = "ssh-keygen",
 		.verify_args = ssh_verify_args,
 		.sigs = ssh_sigs,
-		.verify_signed_buffer = NULL, /* TODO */
+		.verify_signed_buffer = verify_ssh_signed_buffer,
 		.sign_buffer = sign_buffer_ssh
 	},
 };
@@ -343,6 +349,165 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
 	return ret;
 }
 
+static void parse_ssh_output(struct signature_check *sigc)
+{
+	const char *line, *principal, *search;
+
+	/*
+	 * ssh-keysign output should be:
+	 * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
+	 * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT
+	 * or for valid but unknown keys:
+	 * Good "git" signature with RSA key SHA256:FINGERPRINT
+	 */
+	sigc->result = 'B';
+	sigc->trust_level = TRUST_NEVER;
+
+	line = xmemdupz(sigc->output, strcspn(sigc->output, "\n"));
+
+	if (skip_prefix(line, "Good \"git\" signature for ", &line)) {
+		/* Valid signature and known principal */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_FULLY;
+
+		/* Search for the last "with" to get the full principal */
+		principal = line;
+		do {
+			search = strstr(line, " with ");
+			if (search)
+				line = search + 1;
+		} while (search != NULL);
+		sigc->signer = xmemdupz(principal, line - principal - 1);
+		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+		sigc->key = xstrdup(sigc->fingerprint);
+	} else if (skip_prefix(line, "Good \"git\" signature with ", &line)) {
+		/* Valid signature, but key unknown */
+		sigc->result = 'G';
+		sigc->trust_level = TRUST_UNDEFINED;
+		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
+		sigc->key = xstrdup(sigc->fingerprint);
+	}
+}
+
+static int verify_ssh_signed_buffer(struct signature_check *sigc,
+				    struct gpg_format *fmt, const char *payload,
+				    size_t payload_size, const char *signature,
+				    size_t signature_size)
+{
+	struct child_process ssh_keygen = CHILD_PROCESS_INIT;
+	struct tempfile *buffer_file;
+	int ret = -1;
+	const char *line;
+	size_t trust_size;
+	char *principal;
+	struct strbuf ssh_keygen_out = STRBUF_INIT;
+	struct strbuf ssh_keygen_err = STRBUF_INIT;
+
+	if (!ssh_allowed_signers) {
+		error(_("gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification"));
+		return -1;
+	}
+
+	buffer_file = mks_tempfile_t(".git_vtag_tmpXXXXXX");
+	if (!buffer_file)
+		return error_errno(_("could not create temporary file"));
+	if (write_in_full(buffer_file->fd, signature, signature_size) < 0 ||
+	    close_tempfile_gently(buffer_file) < 0) {
+		error_errno(_("failed writing detached signature to '%s'"),
+			    buffer_file->filename.buf);
+		delete_tempfile(&buffer_file);
+		return -1;
+	}
+
+	/* Find the principal from the signers */
+	strvec_pushl(&ssh_keygen.args, fmt->program,
+		     "-Y", "find-principals",
+		     "-f", ssh_allowed_signers,
+		     "-s", buffer_file->filename.buf,
+		     NULL);
+	ret = pipe_command(&ssh_keygen, NULL, 0, &ssh_keygen_out, 0,
+			   &ssh_keygen_err, 0);
+	if (ret && strstr(ssh_keygen_err.buf, "usage:")) {
+		error(_("ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)"));
+		goto out;
+	}
+	if (ret || !ssh_keygen_out.len) {
+		/* We did not find a matching principal in the allowedSigners - Check
+		 * without validation */
+		child_process_init(&ssh_keygen);
+		strvec_pushl(&ssh_keygen.args, fmt->program,
+			     "-Y", "check-novalidate",
+			     "-n", "git",
+			     "-s", buffer_file->filename.buf,
+			     NULL);
+		ret = pipe_command(&ssh_keygen, payload, payload_size,
+				   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+	} else {
+		/* Check every principal we found (one per line) */
+		for (line = ssh_keygen_out.buf; *line;
+		     line = strchrnul(line + 1, '\n')) {
+			while (*line == '\n')
+				line++;
+			if (!*line)
+				break;
+
+			trust_size = strcspn(line, "\n");
+			principal = xmemdupz(line, trust_size);
+
+			child_process_init(&ssh_keygen);
+			strbuf_release(&ssh_keygen_out);
+			strbuf_release(&ssh_keygen_err);
+			strvec_push(&ssh_keygen.args, fmt->program);
+			/* We found principals - Try with each until we find a
+			 * match */
+			strvec_pushl(&ssh_keygen.args, "-Y", "verify",
+				     "-n", "git",
+				     "-f", ssh_allowed_signers,
+				     "-I", principal,
+				     "-s", buffer_file->filename.buf,
+				     NULL);
+
+			if (ssh_revocation_file) {
+				if (file_exists(ssh_revocation_file)) {
+					strvec_pushl(&ssh_keygen.args, "-r",
+						     ssh_revocation_file, NULL);
+				} else {
+					warning(_("ssh signing revocation file configured but not found: %s"),
+						ssh_revocation_file);
+				}
+			}
+
+			sigchain_push(SIGPIPE, SIG_IGN);
+			ret = pipe_command(&ssh_keygen, payload, payload_size,
+					   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
+			sigchain_pop(SIGPIPE);
+
+			FREE_AND_NULL(principal);
+
+			ret &= starts_with(ssh_keygen_out.buf, "Good");
+			if (ret == 0)
+				break;
+		}
+	}
+
+	sigc->payload = xmemdupz(payload, payload_size);
+	strbuf_stripspace(&ssh_keygen_out, 0);
+	strbuf_stripspace(&ssh_keygen_err, 0);
+	strbuf_add(&ssh_keygen_out, ssh_keygen_err.buf, ssh_keygen_err.len);
+	sigc->output = strbuf_detach(&ssh_keygen_out, NULL);
+	sigc->gpg_status = xstrdup(sigc->output);
+
+	parse_ssh_output(sigc);
+
+out:
+	if (buffer_file)
+		delete_tempfile(&buffer_file);
+	strbuf_release(&ssh_keygen_out);
+	strbuf_release(&ssh_keygen_err);
+
+	return ret;
+}
+
 int check_signature(const char *payload, size_t plen, const char *signature,
 	size_t slen, struct signature_check *sigc)
 {
@@ -453,6 +618,18 @@ int git_gpg_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (!strcmp(var, "gpg.ssh.allowedsignersfile")) {
+		if (!value)
+			return config_error_nonbool(var);
+		return git_config_string(&ssh_allowed_signers, var, value);
+	}
+
+	if (!strcmp(var, "gpg.ssh.revocationFile")) {
+		if (!value)
+			return config_error_nonbool(var);
+		return git_config_string(&ssh_revocation_file, var, value);
+	}
+
 	if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
 		fmtname = "openpgp";
 
-- 
gitgitgadget

[PATCH v6 6/9] ssh signing: add test prereqs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:37:02

From: Fabian Stelzer <redacted>

generate some ssh keys and a allowedSignersFile for testing

Signed-off-by: Fabian Stelzer <redacted>
---
 t/lib-gpg.sh | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index 9fc5241228e..600c8d1a026 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -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;
-- 
gitgitgadget

[PATCH v6 4/9] ssh signing: provide a textual representation of the signing key

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:37:03

From: Fabian Stelzer <redacted>

for ssh the user.signingkey can be a filename/path or even a literal ssh pubkey.
in push certs and textual output we prefer the ssh fingerprint instead.

Signed-off-by: Fabian Stelzer <redacted>
---
 gpg-interface.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 gpg-interface.h |  6 ++++++
 send-pack.c     |  8 ++++----
 3 files changed, 56 insertions(+), 4 deletions(-)
diff --git a/gpg-interface.c b/gpg-interface.c
index 3afacb48900..ec48a37b6cc 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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)
diff --git a/gpg-interface.h b/gpg-interface.h
index feac4decf8b..beefacbb1e9 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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);
diff --git a/send-pack.c b/send-pack.c
index 5a79e0e7110..50cca7e439b 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -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;
 }
-- 
gitgitgadget

[PATCH v6 8/9] ssh signing: add more tests for logs, tags & push certs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:37:04

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 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
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 39e746fbcbe..afd7f2516ee 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -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 &&
diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh
index bba768f5ded..d590249b995 100755
--- a/t/t5534-push-signed.sh
+++ b/t/t5534-push-signed.sh
@@ -137,6 +137,53 @@ 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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 with number 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 &&
@@ -276,6 +323,60 @@ 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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 hasnokey@nowhere.com &&
+	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 with number 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 &&
diff --git a/t/t7031-verify-tag-signed-ssh.sh b/t/t7031-verify-tag-signed-ssh.sh
new file mode 100755
index 00000000000..05bf520a332
--- /dev/null
+++ b/t/t7031-verify-tag-signed-ssh.sh
@@ -0,0 +1,161 @@
+#!/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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	(
+		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.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 &&
+	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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	(
+		for tag in initial second merge fourth-signed sixth-signed seventh-signed
+		do
+			git verify-tag --raw $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 --raw $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 --raw $tag 2>actual &&
+			grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
+			! grep "${BAD_SIGNATURE}" actual &&
+			echo $tag OK || exit 1
+		done
+	)
+'
+
+test_expect_success GPGSSH 'verify signatures with --raw ssh' '
+	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 &&
+	echo sixth-signed OK
+'
+
+test_expect_success GPGSSH 'verify multiple tags ssh' '
+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	tags="seventh-signed sixth-signed" &&
+	for i in $tags
+	do
+		git verify-tag -v --raw $i || return 1
+	done >expect.stdout 2>expect.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <expect.stderr.1 >expect.stderr &&
+	git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
+	grep "^${GOOD_SIGNATURE_TRUSTED}" <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 - ssh' '
+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
+	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 - ssh' '
+	test_must_fail git verify-tag --format="tagname : %(tag)" $(cat forged1.tag) >actual-forged &&
+	test_must_be_empty actual-forged
+'
+
+test_done
-- 
gitgitgadget

[PATCH v6 9/9] ssh signing: add documentation

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:37:05

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 Documentation/config/gpg.txt  | 39 +++++++++++++++++++++++++++++++++--
 Documentation/config/user.txt |  6 ++++++
 2 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/Documentation/config/gpg.txt b/Documentation/config/gpg.txt
index d94025cb368..dc790512e86 100644
--- a/Documentation/config/gpg.txt
+++ b/Documentation/config/gpg.txt
@@ -11,13 +11,13 @@ gpg.program::
 
 gpg.format::
 	Specifies which key format to use when signing with `--gpg-sign`.
-	Default is "openpgp" and another possible value is "x509".
+	Default is "openpgp". Other possible values are "x509", "ssh".
 
 gpg.<format>.program::
 	Use this to customize the program used for the signing format you
 	chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
 	be used as a legacy synonym for `gpg.openpgp.program`. The default
-	value for `gpg.x509.program` is "gpgsm".
+	value for `gpg.x509.program` is "gpgsm" and `gpg.ssh.program` is "ssh-keygen".
 
 gpg.minTrustLevel::
 	Specifies a minimum trust level for signature verification.  If
@@ -33,3 +33,38 @@ gpg.minTrustLevel::
 * `marginal`
 * `fully`
 * `ultimate`
+
+gpg.ssh.allowedSignersFile::
+	A file containing ssh public keys which you are willing to trust.
+	The file consists of one or more lines of principals followed by an ssh
+	public key.
+	e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...
+	See ssh-keygen(1) "ALLOWED SIGNERS" for details.
+	The principal is only used to identify the key and is available when
+	verifying a signature.
++
+SSH has no concept of trust levels like gpg does. To be able to differentiate
+between valid signatures and trusted signatures the trust level of a signature
+verification is set to `fully` when the public key is present in the allowedSignersFile.
+Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.
+Otherwise valid but untrusted signatures will still verify but show no principal
+name of the signer.
++
+This file can be set to a location outside of the repository and every developer
+maintains their own trust store. A central repository server could generate this
+file automatically from ssh keys with push access to verify the code against.
+In a corporate setting this file is probably generated at a global location
+from automation that already handles developer ssh keys.
++
+A repository that only allows signed commits can store the file
+in the repository itself using a path relative to the top-level of the working tree.
+This way only committers with an already valid key can add or change keys in the keyring.
++
+Using a SSH CA key with the cert-authority option
+(see ssh-keygen(1) "CERTIFICATES") is also valid.
+
+gpg.ssh.revocationFile::
+	Either a SSH KRL or a list of revoked public keys (without the principal prefix).
+	See ssh-keygen(1) for details.
+	If a public key is found in this file then it will always be treated
+	as having trust level "never" and signatures will show as invalid.
diff --git a/Documentation/config/user.txt b/Documentation/config/user.txt
index 59aec7c3aed..b3c2f2c541e 100644
--- a/Documentation/config/user.txt
+++ b/Documentation/config/user.txt
@@ -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.
-- 
gitgitgadget

[PATCH v6 7/9] ssh signing: duplicate t7510 tests for commits

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-07-28 19:37:05

From: Fabian Stelzer <redacted>

Signed-off-by: Fabian Stelzer <redacted>
---
 t/t7528-signed-commit-ssh.sh | 398 +++++++++++++++++++++++++++++++++++
 1 file changed, 398 insertions(+)
 create mode 100755 t/t7528-signed-commit-ssh.sh
diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh
new file mode 100755
index 00000000000..e2c48f69e6d
--- /dev/null
+++ b/t/t7528-signed-commit-ssh.sh
@@ -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 <author@example.com> 1112912653 -0700
+	Qcommitter C O Mitter <committer@example.com> 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
-- 
gitgitgadget

Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up

From: Jonathan Tan <hidden>
Date: 2021-07-28 22:32:14

I think this patch set is beyond the "is this a good idea in general"
phase (in particular, I think that being able to sign Git commits by
using SSH infrastructure is very useful), so I'll proceed to critiquing
the commits in more detail.

Firstly, in commit messages, the left side of the colon is usually the
name of the subsystem - in this case, "gpg-interface".
To be able to implement new signing formats this commit:
 - makes the sigc structure more generic by renaming "gpg_output" to
   "output"
 - introduces function pointers in the gpg_format structure to call
   format specific signing and verification functions
 - moves format detection from verify_signed_buffer into the check_signature
   api function and calls the format specific verify
 - renames and wraps sign_buffer to handle format specific signing logic
   as well
I think that this commit should be further split up - in particular, it
is hard for reviewers to verify that there is no difference in
functionality before and after this commit. I already spotted one
difference - perhaps there are more. For me, splitting the above 4
points into 4 commits would be an acceptable split.
quoted hunk
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..31cf4ba3938 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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]
 static struct gpg_format gpg_format[] = {
-	{ .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,
 	},
 };
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.
quoted hunk
@@ -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 hunk
@@ -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_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

From: Jonathan Tan <hidden>
Date: 2021-07-28 22:45:30

Keep the commit titles to 50 characters or fewer. E.g.:

  gpg-interface: teach "ssh" gpg.format
implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function
Capitalization and punctuation.
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.

gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.
I notice that end-user documentation (e.g. about gpg.ssh.program) is in
its own patch, but could that be added as functionality is being
implemented? That makes it easier for reviewers to understand what's
being implemented in each patch.
quoted hunk
@@ -463,12 +482,30 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
 	return use_format->sign_buffer(buffer, signature, signing_key);
 }
 
+/*
+ * Strip CR from the line endings, in case we are on Windows.
+ * NEEDSWORK: make it trim only CRs before LFs and rename
+ */
+static void remove_cr_after(struct strbuf *buffer, size_t offset)
+{
+	size_t i, j;
+
+	for (i = j = offset; i < buffer->len; i++) {
+		if (buffer->buf[i] != '\r') {
+			if (i != j)
+				buffer->buf[j] = buffer->buf[i];
+			j++;
+		}
+	}
+	strbuf_setlen(buffer, j);
+}
In the future, I would prefer refactoring like this to be in its own
patch. For the moment, this should probably be called "remove_cr" (no
"after" as CRs are removed wherever they are in the string).
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+			   const char *signing_key)
+{
+	struct child_process signer = CHILD_PROCESS_INIT;
+	int ret = -1;
+	size_t bottom, keylen;
+	struct strbuf signer_stderr = STRBUF_INIT;
+	struct tempfile *key_file = NULL, *buffer_file = NULL;
+	char *ssh_signing_key_file = NULL;
+	struct strbuf ssh_signature_filename = STRBUF_INIT;
+
+	if (!signing_key || signing_key[0] == '\0')
+		return error(
+			_("user.signingkey needs to be set for ssh signing"));
+
+	if (starts_with(signing_key, "ssh-")) {
+		/* A literal ssh key */
+		key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
+		if (!key_file)
+			return error_errno(
+				_("could not create temporary file"));
+		keylen = strlen(signing_key);
+		if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
+		    close_tempfile_gently(key_file) < 0) {
+			error_errno(_("failed writing ssh signing key to '%s'"),
+				    key_file->filename.buf);
+			goto out;
+		}
+		ssh_signing_key_file = key_file->filename.buf;
+	} else {
+		/* We assume a file */
+		ssh_signing_key_file = expand_user_path(signing_key, 1);
+	}
A config that has 2 modes of operation is quite error-prone, I think.
For example, a user could put a path starting with "ssh-" (admittedly
unlikely since it would usually be an absolute path, but not
impossible). And also from an implementation point of view, here the
"ssh-" is case-sensitive, but in a future patch, there is a "ssh-" that
is case-insensitive.

Can this just always take a path?
+	if (ret) {
+		if (strstr(signer_stderr.buf, "usage:"))
+			error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
+
+		error("%s", signer_stderr.buf);
+		goto out;
+	}
Checking for "usage:" seems fragile -  a binary running in a different
locale might emit a different string, and legitimate output may somehow
contain the string "usage:". Is there a different way to detect a
version mismatch?

Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent

From: Jonathan Tan <hidden>
Date: 2021-07-28 22:48:36

if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
[snip]
+/* Returns the first public key from an ssh-agent to use for signing */
+static char *get_default_ssh_signing_key(void)
+{
+	struct child_process ssh_add = CHILD_PROCESS_INIT;
+	int ret = -1;
+	struct strbuf key_stdout = STRBUF_INIT;
+	struct strbuf **keys;
+
+	strvec_pushl(&ssh_add.args, "ssh-add", "-L", NULL);
+	ret = pipe_command(&ssh_add, NULL, 0, &key_stdout, 0, NULL, 0);
+	if (!ret) {
+		keys = strbuf_split_max(&key_stdout, '\n', 2);
+		if (keys[0])
+			return strbuf_detach(keys[0], NULL);
+	}
+
+	strbuf_release(&key_stdout);
+	return "";
+}
Could the commit message have a better explanation of why we need this?
(Also, I would think that the command being run needs to be configurable
instead of being just the first "ssh-add" in $PATH, and the parsing of
the output should be more rigorous. But this is moot if we don't need
this feature in the first place.)

Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures

From: Jonathan Tan <hidden>
Date: 2021-07-28 23:04:56

to verify a ssh signature we first call ssh-keygen -Y find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers identity.
Is this the same behavior as GPG signing in Git?
Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED
SIGNERS") which contains valid public keys and a principal (usually
user@domain). Depending on the environment this file can be managed by
the individual developer or for example generated by the central
repository server from known ssh keys with push access. If the
repository only allows signed commits / pushes then the file can even be
stored inside it.
Storing the allowedSignersFile in the repo is technically possible even
if the repository does not allow signed commits/pushes, right? I would
reword the last sentence as "This file is usually stored outside the
repository, but if the repository only allows signed commits/pushes, the
user might choose to store it in the repository".
Using SSH CA Keys with these files is also possible. Add
"cert-authority" as key option between the principal and the key to mark
it as a CA and all keys signed by it as valid for this CA.
Is this functionality provided by SSH? I don't see "cert-authority"
anywhere in the diff below.

Also, I notice that the tests are all provided at the end. I think that
it would be better for the tests to be incrementally provided along with
the commit that introduces the relevant functionality, so it is clearer
to the reviewers how it is supposed to work (and also for us to observe
test coverage).
quoted hunk
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index a34742513ac..62b11c5f3a4 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -131,6 +131,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
 {
 	int status = parse_hide_refs_config(var, value, "receive");
 
+	git_gpg_config(var, value, NULL);
+
 	if (status)
 		return status;
Check the return value of git_gpg_config() to see if that config was
processed by that function - if yes, we can return early.
+static void parse_ssh_output(struct signature_check *sigc)
+{
+	const char *line, *principal, *search;
+
+	/*
+	 * ssh-keysign output should be:
+	 * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
+	 * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT
+	 * or for valid but unknown keys:
+	 * Good "git" signature with RSA key SHA256:FINGERPRINT
+	 */
Is this "ssh-keysign" or "ssh-keygen" output?

Also, is this output documented to be stable even across locales?
+	sigc->result = 'B';
+	sigc->trust_level = TRUST_NEVER;
A discussion of trust levels should also be in the commit message or
user documentation.
+	if (!strcmp(var, "gpg.ssh.revocationFile")) {
The "F" in "revocationFile" has to be lowercase. (If tests were
included, as I suggested above, it might have been easier to catch
this.)

Re: [PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Bagas Sanjaya <hidden>
Date: 2021-07-29 08:19:37

On 29/07/21 02.36, Fabian Stelzer via GitGitGadget wrote:
openssh 8.7 will add valid-after, valid-before options to the allowed keys
keyring. This allows us to pass the commit timestamp to the verification
call and make key rollover possible and still be able to verify older
commits. Set valid-after=NOW when adding your key to the keyring and set
valid-before to make it fail if used after a certain date. Software like
gitolite/github or corporate automation can do this automatically when ssh
push keys are addded / removed I will add this feature in a follow up patch
afterwards.
I read above as "set valid-before=<some date> and valid-after=<now> to 
limit key validity for several days from now". Is it right?

-- 
An old man doll... just what I always wanted! - Clara

Re: [PATCH v6 1/9] ssh signing: preliminary refactoring and clean-up

From: Fabian Stelzer <hidden>
Date: 2021-07-29 08:43:43

On 29.07.21 00:32, Jonathan Tan wrote:
I think this patch set is beyond the "is this a good idea in general"
phase (in particular, I think that being able to sign Git commits by
using SSH infrastructure is very useful), so I'll proceed to critiquing
the commits in more detail.
Thanks for your help.
Firstly, in commit messages, the left side of the colon is usually the
name of the subsystem - in this case, "gpg-interface".
The docs call this "name of the component you're working on". Since this 
code does not actually change any gpg functionality (at least it should 
not) i think gpg-interface in the commits might be a bit misleading.

quoted
To be able to implement new signing formats this commit:
  - makes the sigc structure more generic by renaming "gpg_output" to
    "output"
  - introduces function pointers in the gpg_format structure to call
    format specific signing and verification functions
  - moves format detection from verify_signed_buffer into the check_signature
    api function and calls the format specific verify
  - renames and wraps sign_buffer to handle format specific signing logic
    as well
I think that this commit should be further split up - in particular, it
is hard for reviewers to verify that there is no difference in
functionality before and after this commit. I already spotted one
difference - perhaps there are more. For me, splitting the above 4
points into 4 commits would be an acceptable split.
The rename can of course be easily separated. The others would probably 
require some code in between commits that's not present in the final 
patch result to make the individual commits compile / work. Otherwise 
those would only add unused code with the last commit then actually 
using everything. I don't think that would make things easier to verify, 
would it?

[snip]
quoted
  static struct gpg_format gpg_format[] = {
-	{ .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,
  	},
  };
I think that verify_signed_buffer and sign_buffer should replace
verify_args and sigs, not be alongside them. In particular, I see from
later patches that a new entry will be introduced for SSH, and the
corresponding new "verify" function does not use verify_args or sigs.
I kept the verify_args since i would either have to duplicate the 
verify_gpg_signed_buffer for gpg & gpgsm or have an if within deciding 
what format to use.
Also this is something that we might want to make a configuration option 
in the future and pass to ssh-keygen as well (there are a couple of -O 
options for it users might want)

sigs is still needed for the parse_signed_buffer api function.

Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent

From: Fabian Stelzer <hidden>
Date: 2021-07-29 08:59:53

On 29.07.21 00:48, Jonathan Tan wrote:
quoted
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

From: Fabian Stelzer <hidden>
Date: 2021-07-29 09:48:25

On 29.07.21 01:04, Jonathan Tan wrote:
quoted
to verify a ssh signature we first call ssh-keygen -Y find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers identity.
Is this the same behavior as GPG signing in Git?
Not quite. GPG requires every signers public key to be in the keyring. 
But even then, the "UNDEFINED" Trust level is enough to be valid for 
commits (but not for merges).
For SSH i did set the unknown keys to UNDEFINED as well and they will 
show up as valid but not have a principal to identify them.
This way a project can decide wether to accept unknown keys by setting 
the gpg.mintrustlevel. So the default behaviour is different.
The alternative would be to treat unknown keys always as invalid.
quoted
Verification uses the gpg.ssh.allowedSignersFile (see ssh-keygen(1) "ALLOWED
SIGNERS") which contains valid public keys and a principal (usually
user@domain). Depending on the environment this file can be managed by
the individual developer or for example generated by the central
repository server from known ssh keys with push access. If the
repository only allows signed commits / pushes then the file can even be
stored inside it.
Storing the allowedSignersFile in the repo is technically possible even
if the repository does not allow signed commits/pushes, right? I would
reword the last sentence as "This file is usually stored outside the
repository, but if the repository only allows signed commits/pushes, the
user might choose to store it in the repository".
yes, thats correct. I have changed the wording.
quoted
Using SSH CA Keys with these files is also possible. Add
"cert-authority" as key option between the principal and the key to mark
it as a CA and all keys signed by it as valid for this CA.
Is this functionality provided by SSH? I don't see "cert-authority"
anywhere in the diff below.
I'll add "See "CERTIFICATES" in ssh-keygen(1)."
It is a SSH feature that i just wanted to make people aware of.
Also, I notice that the tests are all provided at the end. I think that
it would be better for the tests to be incrementally provided along with
the commit that introduces the relevant functionality, so it is clearer
to the reviewers how it is supposed to work (and also for us to observe
test coverage).
The problem is that nearly all of the tests use both signing & 
verification of signatures. I could move the initial test that creates 
all the signed commits but probably not much else.
quoted
+	git_gpg_config(var, value, NULL);
Check the return value of git_gpg_config() to see if that config was
processed by that function - if yes, we can return early.
fixed

quoted
+static void parse_ssh_output(struct signature_check *sigc)
+{
+	const char *line, *principal, *search;
+
+	/*
+	 * ssh-keysign output should be:
+	 * Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
+	 * Good "git" signature for PRINCIPAL WITH WHITESPACE with RSA key SHA256:FINGERPRINT
+	 * or for valid but unknown keys:
+	 * Good "git" signature with RSA key SHA256:FINGERPRINT
+	 */
Is this "ssh-keysign" or "ssh-keygen" output?
ssh-keygen. ssh-keysign is only used for host keys. But the names can 
get a bit confusing sometimes. i changed it to ssh-keygen here.
Also, is this output documented to be stable even across locales?
Not really :/ (it currently is not locale specific)
The documentation states to only check the commands exit code. Do we 
trust the exit code enough to rely on it for verification?
If so then i can move the main result and only parse the text for the 
signer/fingerprint info thats used in log formats. This way only the 
logs would break in case the output changes.

I added the output check since the gpg code did so as well:
ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
quoted
+	sigc->result = 'B';
+	sigc->trust_level = TRUST_NEVER;
A discussion of trust levels should also be in the commit message or
user documentation.
quoted
+	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

From: Fabian Stelzer <hidden>
Date: 2021-07-29 11:01:27

On 29.07.21 00:45, Jonathan Tan wrote:
Keep the commit titles to 50 characters or fewer. E.g.:

   gpg-interface: teach "ssh" gpg.format
i will go over my commits and shorten them although I find your example 
very unclear. or did you mean: teach "ssh" to gpg.format ?
quoted
implements the actual sign_buffer_ssh operation and move some shared
cleanup code into a strbuf function
Capitalization and punctuation.
fixed
quoted
Set gpg.format = ssh and user.signingkey to either a ssh public key
string (like from an authorized_keys file), or a ssh key file.
If the key file or the config value itself contains only a public key
then the private key needs to be available via ssh-agent.

gpg.ssh.program can be set to an alternative location of ssh-keygen.
A somewhat recent openssh version (8.2p1+) of ssh-keygen is needed for
this feature. Since only ssh-keygen is needed it can this way be
installed seperately without upgrading your system openssh packages.
I notice that end-user documentation (e.g. about gpg.ssh.program) is in
its own patch, but could that be added as functionality is being
implemented? That makes it easier for reviewers to understand what's
being implemented in each patch.
I can move the user.signingkey & gpg.format part into the signing 
implementation commit and the rest into the verification. I don't see 
much benefit in splitting it up further. I don't want to split up parts 
of the same documentation block into separate commits.
quoted
+
+	if (starts_with(signing_key, "ssh-")) {
+		/* A literal ssh key */
+		key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
+		if (!key_file)
+			return error_errno(
+				_("could not create temporary file"));
+		keylen = strlen(signing_key);
+		if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
+		    close_tempfile_gently(key_file) < 0) {
+			error_errno(_("failed writing ssh signing key to '%s'"),
+				    key_file->filename.buf);
+			goto out;
+		}
+		ssh_signing_key_file = key_file->filename.buf;
+	} else {
+		/* We assume a file */
+		ssh_signing_key_file = expand_user_path(signing_key, 1);
+	}
A config that has 2 modes of operation is quite error-prone, I think.
For example, a user could put a path starting with "ssh-" (admittedly
unlikely since it would usually be an absolute path, but not
impossible). And also from an implementation point of view, here the
"ssh-" is case-sensitive, but in a future patch, there is a "ssh-" that
is case-insensitive.

Can this just always take a path?
I found the ability to specify the key literally useful since i don't 
need an extra file for my public key. In my case all keys come from an 
ssh-agent anyway but I'd like to be able to select which one to use for 
signing. But i'm not hard pressed on this feature. If consenus is this 
complicates things then i can remove it.
quoted
+	if (ret) {
+		if (strstr(signer_stderr.buf, "usage:"))
+			error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
+
+		error("%s", signer_stderr.buf);
+		goto out;
+	}
Checking for "usage:" seems fragile -  a binary running in a different
locale might emit a different string, and legitimate output may somehow
contain the string "usage:". Is there a different way to detect a
version mismatch?
I agree. Unfortunately i did not find any better way. But i think the 
risk of doing something wrong here is quite low. We only check for 
"usage:" in case ssh-keygen fails. And all we do if we find it is give 
the user an extra hint on what the problem probably is.
In any case we print the full stderr output as well.

Re: [PATCH v6 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer <hidden>
Date: 2021-07-29 11:03:29

On 29.07.21 10:19, Bagas Sanjaya wrote:
On 29/07/21 02.36, Fabian Stelzer via GitGitGadget wrote:
quoted
openssh 8.7 will add valid-after, valid-before options to the allowed 
keys
keyring. This allows us to pass the commit timestamp to the verification
call and make key rollover possible and still be able to verify older
commits. Set valid-after=NOW when adding your key to the keyring and set
valid-before to make it fail if used after a certain date. Software like
gitolite/github or corporate automation can do this automatically when 
ssh
push keys are addded / removed I will add this feature in a follow up 
patch
afterwards.
I read above as "set valid-before=<some date> and valid-after=<now> to 
limit key validity for several days from now". Is it right?
no. "NOW" is not meant literally but in the sense to add the current 
date when adding the key. I'll edit the description. But this feature in 
general will follow in a separate patchset with proper documentation anyway.

Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer <hidden>
Date: 2021-07-29 13:52:51

On 29.07.21 11:48, Fabian Stelzer wrote:
On 29.07.21 01:04, Jonathan Tan wrote:
quoted
quoted
to verify a ssh signature we first call ssh-keygen -Y find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers identity.
Is this the same behavior as GPG signing in Git?
Not quite. GPG requires every signers public key to be in the keyring. 
But even then, the "UNDEFINED" Trust level is enough to be valid for 
commits (but not for merges).
For SSH i did set the unknown keys to UNDEFINED as well and they will 
show up as valid but not have a principal to identify them.
This way a project can decide wether to accept unknown keys by setting 
the gpg.mintrustlevel. So the default behaviour is different.
The alternative would be to treat unknown keys always as invalid.
I thought a bit more about this and my approach is indeed problematic 
especially when a repo has both gpg and ssh signatures. The trust level 
setting can then not behave differently for both.

My intention of still showing valid but unknown signatures in the log as 
ok (but unknown) was to encourage users to always sign their work even 
if they are not (yet) trusted in the allowedSignersFile.

I think the way forward should be to treat unknown singing keys as not 
verified like gpg does.

If a ssh key is verified and in the allowedSignersFile i would still set 
its trust level to "FULLY".

Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys

From: Josh Steadmon <hidden>
Date: 2021-07-29 19:09:10

Thanks for this series, it sounds like a great idea. I have a few
comments, inline below.

On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:
[snip]
+static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
+			   const char *signing_key)
+{
+	struct child_process signer = CHILD_PROCESS_INIT;
+	int ret = -1;
+	size_t bottom, keylen;
+	struct strbuf signer_stderr = STRBUF_INIT;
+	struct tempfile *key_file = NULL, *buffer_file = NULL;
+	char *ssh_signing_key_file = NULL;
+	struct strbuf ssh_signature_filename = STRBUF_INIT;
+
+	if (!signing_key || signing_key[0] == '\0')
+		return error(
+			_("user.signingkey needs to be set for ssh signing"));
+
+	if (starts_with(signing_key, "ssh-")) {
+		/* A literal ssh key */
+		key_file = mks_tempfile_t(".git_signing_key_tmpXXXXXX");
+		if (!key_file)
+			return error_errno(
+				_("could not create temporary file"));
+		keylen = strlen(signing_key);
+		if (write_in_full(key_file->fd, signing_key, keylen) < 0 ||
+		    close_tempfile_gently(key_file) < 0) {
+			error_errno(_("failed writing ssh signing key to '%s'"),
+				    key_file->filename.buf);
+			goto out;
+		}
+		ssh_signing_key_file = key_file->filename.buf;
You probably want to call strbuf_detach() here, because...
+	} else {
+		/* We assume a file */
+		ssh_signing_key_file = expand_user_path(signing_key, 1);
+	}
... you need to free the memory returned by expand_user_path(). If you
detach the strbuf above, you can unconditionally
free(ssh_signing_key_file) at the end of this function.
+
+	buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
+	if (!buffer_file) {
+		error_errno(_("could not create temporary file"));
+		goto out;
+	}
+
+	if (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||
+	    close_tempfile_gently(buffer_file) < 0) {
+		error_errno(_("failed writing ssh signing key buffer to '%s'"),
+			    buffer_file->filename.buf);
+		goto out;
+	}
+
+	strvec_pushl(&signer.args, use_format->program,
+		     "-Y", "sign",
+		     "-n", "git",
+		     "-f", ssh_signing_key_file,
+		     buffer_file->filename.buf,
+		     NULL);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	ret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);
+	sigchain_pop(SIGPIPE);
+
+	if (ret) {
+		if (strstr(signer_stderr.buf, "usage:"))
+			error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
I share Jonathan Tan's concern about checking for "usage:" in the stderr
output here. I think in patch 6 the tests rely on a specific return code
to check that "-Y sign" is working as expected; can that be used here
instead?
+
+		error("%s", signer_stderr.buf);
+		goto out;
+	}
+
+	bottom = signature->len;
+
+	strbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);
+	strbuf_addstr(&ssh_signature_filename, ".sig");
+	if (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {
+		error_errno(
+			_("failed reading ssh signing data buffer from '%s'"),
+			ssh_signature_filename.buf);
+	}
+	unlink_or_warn(ssh_signature_filename.buf);
+
+	/* Strip CR from the line endings, in case we are on Windows. */
+	remove_cr_after(signature, bottom);
+
+out:
+	if (key_file)
+		delete_tempfile(&key_file);
+	if (buffer_file)
+		delete_tempfile(&buffer_file);
+	strbuf_release(&signer_stderr);
+	strbuf_release(&ssh_signature_filename);
+	return ret;
+}
-- 
gitgitgadget

Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent

From: Josh Steadmon <hidden>
Date: 2021-07-29 19:09:31

On 2021.07.29 10:59, Fabian Stelzer wrote:
On 29.07.21 00:48, Jonathan Tan wrote:
quoted
quoted
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
[snip]

Could the commit message have a better explanation of why we need this?
(Also, I would think that the command being run needs to be configurable
instead of being just the first "ssh-add" in $PATH, and the parsing of
the output should be more rigorous. But this is moot if we don't need
this feature in the first place.)
How about:
If user.signingkey ist not set and a ssh signature is requested we call
ssh-add -L und use the first key we get. This enables us to activate commit
signing globally for all users on a shared server when ssh-agent forwarding
is already in use without the need to touch an individual users gitconfig.

Maybe a general gpg.ssh.signingKeyDefaultCommand that we call and use the
first returned line as key would be useful and achieve the same goal without
having this default for everyone.
On the other hand i like having less configuration / good defaults for
individual users. But I'm coming from a corporate environment, not an open
source project.
Doesn't this run the risk of using the wrong key (and potentially
exposing someone's identity)? On my work machine, my corporate SSH key
is not actually the first key in my SSH agent.

Rather than making this behavior the default, could it instead be
enabled only if the signing key is set to "use-ssh-agent" or something
similar?

Re: [PATCH v6 6/9] ssh signing: add test prereqs

From: Josh Steadmon <hidden>
Date: 2021-07-29 19:09:56

On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:
quoted hunk
From: Fabian Stelzer <redacted>

generate some ssh keys and a allowedSignersFile for testing

Signed-off-by: Fabian Stelzer <redacted>
---
 t/lib-gpg.sh | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index 9fc5241228e..600c8d1a026 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -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"
+
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.

 sanitize_pgp() {
 	perl -ne '
 		/^-----END PGP/ and $in_pgp = 0;
-- 
gitgitgadget

Re: [PATCH v6 3/9] ssh signing: retrieve a default key from ssh-agent

From: Fabian Stelzer <hidden>
Date: 2021-07-29 21:21:22

On 29.07.21 21:09, Josh Steadmon wrote:
On 2021.07.29 10:59, Fabian Stelzer wrote:
quoted
On 29.07.21 00:48, Jonathan Tan wrote:
quoted
quoted
if user.signingkey is not set and a ssh signature is requested we call
ssh-add -L and use the first key we get
[snip]

Could the commit message have a better explanation of why we need this?
(Also, I would think that the command being run needs to be configurable
instead of being just the first "ssh-add" in $PATH, and the parsing of
the output should be more rigorous. But this is moot if we don't need
this feature in the first place.)
How about:
If user.signingkey ist not set and a ssh signature is requested we call
ssh-add -L und use the first key we get. This enables us to activate commit
signing globally for all users on a shared server when ssh-agent forwarding
is already in use without the need to touch an individual users gitconfig.

Maybe a general gpg.ssh.signingKeyDefaultCommand that we call and use the
first returned line as key would be useful and achieve the same goal without
having this default for everyone.
On the other hand i like having less configuration / good defaults for
individual users. But I'm coming from a corporate environment, not an open
source project.
Doesn't this run the risk of using the wrong key (and potentially
exposing someone's identity)? On my work machine, my corporate SSH key
is not actually the first key in my SSH agent.

Rather than making this behavior the default, could it instead be
enabled only if the signing key is set to "use-ssh-agent" or something
similar?
If we introduce a signingKeyDefaultComand we don't need the 
"use-ssh-agent" flag.

If user.signingkey is set it is used no matter what. A private key needs 
to be available either in the specified file or via ssh agent.

If it is not set then an automatic way to get a default key would be great.
So if we set signingKeyDefaultCommand to "ssh-add" (or a script 
returning a key) then the first available key could be used.
If this variable is unset and no user.signingkey is specified we fail 
and tell the user to set a signingkey.

If this variable is set to "ssh-add" by default or unset and needs to be
set explicitly set to have an automatic default key can be decided.

Re: [PATCH v6 2/9] ssh signing: add ssh signature format and signing using ssh keys

From: Fabian Stelzer <hidden>
Date: 2021-07-29 21:26:02

On 29.07.21 21:09, Josh Steadmon wrote:
Thanks for this series, it sounds like a great idea. I have a few
comments, inline below.
Thanks for your review and help with this patch.
On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:
[snip]
quoted
+		ssh_signing_key_file = key_file->filename.buf;
You probably want to call strbuf_detach() here, because...
quoted
+	} else {
+		/* We assume a file */
+		ssh_signing_key_file = expand_user_path(signing_key, 1);
+	}
... you need to free the memory returned by expand_user_path(). If you
detach the strbuf above, you can unconditionally
free(ssh_signing_key_file) at the end of this function.
fixed. thanks
quoted
+
+	buffer_file = mks_tempfile_t(".git_signing_buffer_tmpXXXXXX");
+	if (!buffer_file) {
+		error_errno(_("could not create temporary file"));
+		goto out;
+	}
+
+	if (write_in_full(buffer_file->fd, buffer->buf, buffer->len) < 0 ||
+	    close_tempfile_gently(buffer_file) < 0) {
+		error_errno(_("failed writing ssh signing key buffer to '%s'"),
+			    buffer_file->filename.buf);
+		goto out;
+	}
+
+	strvec_pushl(&signer.args, use_format->program,
+		     "-Y", "sign",
+		     "-n", "git",
+		     "-f", ssh_signing_key_file,
+		     buffer_file->filename.buf,
+		     NULL);
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+	ret = pipe_command(&signer, NULL, 0, NULL, 0, &signer_stderr, 0);
+	sigchain_pop(SIGPIPE);
+
+	if (ret) {
+		if (strstr(signer_stderr.buf, "usage:"))
+			error(_("ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"));
I share Jonathan Tan's concern about checking for "usage:" in the stderr
output here. I think in patch 6 the tests rely on a specific return code
to check that "-Y sign" is working as expected; can that be used here
instead?
In the test setup i first check if ssh-keygen at all is present (exit 
code 127 means command not found). Afterwards i check for a specific 
error message from the command if it is present. Not sure how portable 
this is, but i can do that because i give known invalid parameters to 
it. I can't do this here without doing an additional call to ssh-keygen 
just to check this.
quoted
+
+		error("%s", signer_stderr.buf);
+		goto out;
+	}
+
+	bottom = signature->len;
+
+	strbuf_addbuf(&ssh_signature_filename, &buffer_file->filename);
+	strbuf_addstr(&ssh_signature_filename, ".sig");
+	if (strbuf_read_file(signature, ssh_signature_filename.buf, 0) < 0) {
+		error_errno(
+			_("failed reading ssh signing data buffer from '%s'"),
+			ssh_signature_filename.buf);
+	}
+	unlink_or_warn(ssh_signature_filename.buf);
+
+	/* Strip CR from the line endings, in case we are on Windows. */
+	remove_cr_after(signature, bottom);
+
+out:
+	if (key_file)
+		delete_tempfile(&key_file);
+	if (buffer_file)
+		delete_tempfile(&buffer_file);
+	strbuf_release(&signer_stderr);
+	strbuf_release(&ssh_signature_filename);
+	return ret;
+}
-- 
gitgitgadget

Re: [PATCH v6 6/9] ssh signing: add test prereqs

From: Fabian Stelzer <hidden>
Date: 2021-07-30 07:33:08

On 29.07.21 21:09, Josh Steadmon wrote:
On 2021.07.28 19:36, Fabian Stelzer via GitGitGadget wrote:
quoted
From: Fabian Stelzer <redacted>
  
+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"
+
Is there a reason why we don't use these variables in the script above?

Also, in general I feel that it's better to add tests in the same commit
where new features are added, rather than having standalone test
commits.
Intially i wanted to fill them in the prereq but couldn't acces them in 
the tests then.

Thanks, i have moved the variables above the prereq and used them there 
as well. makes sense.
Also i have prefixed them now with GPGSSH so we don't collide with any 
other tests accidentally.

Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer <hidden>
Date: 2021-08-03 07:43:29


On 29.07.21 15:52, Fabian Stelzer wrote:
On 29.07.21 11:48, Fabian Stelzer wrote:
quoted
On 29.07.21 01:04, Jonathan Tan wrote:
quoted
quoted
to verify a ssh signature we first call ssh-keygen -Y find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers identity.
Is this the same behavior as GPG signing in Git?
Not quite. GPG requires every signers public key to be in the keyring. 
But even then, the "UNDEFINED" Trust level is enough to be valid for 
commits (but not for merges).
For SSH i did set the unknown keys to UNDEFINED as well and they will 
show up as valid but not have a principal to identify them.
This way a project can decide wether to accept unknown keys by setting 
the gpg.mintrustlevel. So the default behaviour is different.
The alternative would be to treat unknown keys always as invalid.
I thought a bit more about this and my approach is indeed problematic 
especially when a repo has both gpg and ssh signatures. The trust level 
setting can then not behave differently for both.

My intention of still showing valid but unknown signatures in the log as 
ok (but unknown) was to encourage users to always sign their work even 
if they are not (yet) trusted in the allowedSignersFile.

I think the way forward should be to treat unknown singing keys as not 
verified like gpg does.

If a ssh key is verified and in the allowedSignersFile i would still set 
its trust level to "FULLY".
i dug a bit deeper into the gpg code/tests and it actually already 
behaves the same. untrusted signatures still return successfull on a 
verify-commit/tag even if the key is completely untrusted. my patch does 
the same thing for ssh signatures. i'll send a new revision later today 
with all the other fixes.

Re: [PATCH v6 5/9] ssh signing: parse ssh-keygen output and verify signatures

From: Fabian Stelzer <hidden>
Date: 2021-08-03 09:33:31


On 03.08.21 09:43, Fabian Stelzer wrote:

On 29.07.21 15:52, Fabian Stelzer wrote:
quoted
On 29.07.21 11:48, Fabian Stelzer wrote:
quoted
On 29.07.21 01:04, Jonathan Tan wrote:
quoted
quoted
to verify a ssh signature we first call ssh-keygen -Y 
find-principal to
look up the signing principal by their public key from the
allowedSignersFile. If the key is found then we do a verify. Otherwise
we only validate the signature but can not verify the signers 
identity.
Is this the same behavior as GPG signing in Git?
Not quite. GPG requires every signers public key to be in the 
keyring. But even then, the "UNDEFINED" Trust level is enough to be 
valid for commits (but not for merges).
For SSH i did set the unknown keys to UNDEFINED as well and they will 
show up as valid but not have a principal to identify them.
This way a project can decide wether to accept unknown keys by 
setting the gpg.mintrustlevel. So the default behaviour is different.
The alternative would be to treat unknown keys always as invalid.
I thought a bit more about this and my approach is indeed problematic 
especially when a repo has both gpg and ssh signatures. The trust 
level setting can then not behave differently for both.

My intention of still showing valid but unknown signatures in the log 
as ok (but unknown) was to encourage users to always sign their work 
even if they are not (yet) trusted in the allowedSignersFile.

I think the way forward should be to treat unknown singing keys as not 
verified like gpg does.

If a ssh key is verified and in the allowedSignersFile i would still 
set its trust level to "FULLY".
i dug a bit deeper into the gpg code/tests and it actually already 
behaves the same. untrusted signatures still return successfull on a 
verify-commit/tag even if the key is completely untrusted. my patch does 
the same thing for ssh signatures. i'll send a new revision later today 
with all the other fixes.
oh boy... sorry for all the emails. the gpg stuff can be really 
confusing. especially since there's different meanings of "untrusted", 
"unknown" and "undefined" depending on which docs/codebase you look 
into. Especially "untrusted" is not really a gpg term but used in the 
codebase in tests like 'verify-commit exits success on untrusted 
signature' which tests for a key already in the keyring but not with any 
specified trust level. I could not actually find any gpg test for a 
signature that is completely unknown. (i will add one)

GPG does a successful verify-commit/tag on keys that are "known". 
Meaning that to be marked as good signatures all you need is to have the 
public key in your keyring. This key can still have an unknown/undefined 
trust level (meaning its in the keyring but no decision on trust has 
been made). A key thats not in the keyring has no trustlevel or anything 
but fails hard with "no public key".

SSH signing does not really make this distinction. A key is either in 
the allowedSigners file (and therefore trusted), completely unknown, or 
revoked via the revokedSigners file.
To make this behave like gpg does i will make verification fail on 
completely unknown keys. There is no use of the undefined trust level 
for ssh then and i will set keys in the allowedSigners file to fully 
trusted so they will be accepted for merges as well. I don't see any way 
to have keys that are valid for commits but not merge with ssh then but 
that should be the only difference to gpg.

[PATCH v7 1/9] ssh signing: preliminary refactoring and clean-up

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-08-03 13:46:06

From: Fabian Stelzer <redacted>

Openssh v8.2p1 added some new options to ssh-keygen for signature
creation and verification. These allow us to use ssh keys for git
signatures easily.

In our corporate environment we use PIV x509 Certs on Yubikeys for email
signing/encryption and ssh keys which I think is quite common
(at least for the email part). This way we can establish the correct
trust for the SSH Keys without setting up a separate GPG Infrastructure
(which is still quite painful for users) or implementing x509 signing
support for git (which lacks good forwarding mechanisms).
Using ssh agent forwarding makes this feature easily usable in todays
development environments where code is often checked out in remote VMs / containers.
In such a setup the keyring & revocationKeyring can be centrally
generated from the x509 CA information and distributed to the users.

To be able to implement new signing formats this commit:
 - makes the sigc structure more generic by renaming "gpg_output" to
   "output"
 - introduces function pointers in the gpg_format structure to call
   format specific signing and verification functions
 - moves format detection from verify_signed_buffer into the check_signature
   api function and calls the format specific verify
 - renames and wraps sign_buffer to handle format specific signing logic
   as well

Signed-off-by: Fabian Stelzer <redacted>
---
 fmt-merge-msg.c |   6 +--
 gpg-interface.c | 104 +++++++++++++++++++++++++++++-------------------
 gpg-interface.h |   2 +-
 log-tree.c      |   8 ++--
 pretty.c        |   4 +-
 5 files changed, 74 insertions(+), 50 deletions(-)
diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c
index 0f66818e0f8..fb300bb4b67 100644
--- a/fmt-merge-msg.c
+++ b/fmt-merge-msg.c
@@ -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);
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 127aecfc2b0..db54b054162 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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);
 };
 
 static const char *openpgp_verify_args[] = {
@@ -35,14 +41,29 @@ static const char *x509_sigs[] = {
 	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,
-	  .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,
 	},
 };
 
@@ -72,7 +93,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 +278,16 @@ 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_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_stdout = STRBUF_INIT;
+	struct strbuf gpg_stderr = STRBUF_INIT;
 
 	temp = mks_tempfile_t(".git_vtag_tmpXXXXXX");
 	if (!temp)
@@ -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);
-
 	strvec_push(&gpg.args, fmt->program);
 	strvec_pushv(&gpg.args, fmt->verify_args);
 	strvec_pushl(&gpg.args,
@@ -290,18 +307,22 @@ static int verify_signed_buffer(const char *payload, size_t payload_size,
 		     "--verify", temp->filename.buf, "-",
 		     NULL);
 
-	if (!gpg_status)
-		gpg_status = &buf;
-
 	sigchain_push(SIGPIPE, SIG_IGN);
-	ret = pipe_command(&gpg, payload, payload_size,
-			   gpg_status, 0, gpg_output, 0);
+	ret = pipe_command(&gpg, payload, payload_size, &gpg_stdout, 0,
+			   &gpg_stderr, 0);
 	sigchain_pop(SIGPIPE);
 
 	delete_tempfile(&temp);
 
-	ret |= !strstr(gpg_status->buf, "\n[GNUPG:] GOODSIG ");
-	strbuf_release(&buf); /* no matter it was used or not */
+	ret |= !strstr(gpg_stdout.buf, "\n[GNUPG:] GOODSIG ");
+	sigc->payload = xmemdupz(payload, payload_size);
+	sigc->output = strbuf_detach(&gpg_stderr, NULL);
+	sigc->gpg_status = strbuf_detach(&gpg_stdout, NULL);
+
+	parse_gpg_output(sigc);
+
+	strbuf_release(&gpg_stdout);
+	strbuf_release(&gpg_stderr);
 
 	return ret;
 }
@@ -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_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)
+		die(_("bad/incompatible signature '%s'"), signature);
+
+	status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
+					   slen);
+
+	if (status && !sigc->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;
+	const char *output = flags & GPG_VERIFY_RAW ? sigc->gpg_status :
+							    sigc->output;
 
 	if (flags & GPG_VERIFY_VERBOSE && sigc->payload)
 		fputs(sigc->payload, stdout);
@@ -441,6 +459,12 @@ const char *get_signing_key(void)
 }
 
 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;
diff --git a/gpg-interface.h b/gpg-interface.h
index 80567e48948..feac4decf8b 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -17,7 +17,7 @@ enum signature_trust_level {
 
 struct signature_check {
 	char *payload;
-	char *gpg_output;
+	char *output;
 	char *gpg_status;
 
 	/*
diff --git a/log-tree.c b/log-tree.c
index 6dc4412268b..644893fd8cf 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -515,10 +515,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:
@@ -585,8 +585,8 @@ static int show_one_mergetag(struct commit *commit,
 		/* could have a good signature */
 		status = check_signature(payload.buf, payload.len,
 					 signature.buf, signature.len, &sigc);
-		if (sigc.gpg_output)
-			strbuf_addstr(&verify_message, sigc.gpg_output);
+		if (sigc.output)
+			strbuf_addstr(&verify_message, sigc.output);
 		else
 			strbuf_addstr(&verify_message, "No signature\n");
 		signature_check_clear(&sigc);
diff --git a/pretty.c b/pretty.c
index 9631529c10a..be477bd51f2 100644
--- a/pretty.c
+++ b/pretty.c
@@ -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) {
-- 
gitgitgadget

[PATCH v7 0/9] ssh signing: Add commit & tag signing/verification via SSH keys using ssh-keygen

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-08-03 13:46:07

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 to the current date 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

v7:

 * change unknown signing key behavior to fail verify-commit/tag just like
   gpg does
 * add test for unknown signing keys for ssh & gpg
 * made default signing key retrieval configurable
   (gpg.ssh.defaultKeyCommand). We could default this to "ssh-add -L" but
   would risk some users signing with a wrong key
 * die() instead of error in case of incompatible signatures to match
   current BUG() behaviour more
 * various review fixes (early return for config parse, missing free,
   comments)
 * got rid of strcmp("ssh") branches and used format configurable callbacks
   everywhere
 * moved documentation changes into the commits adding the specific
   functionality

The test 'verify-commit verifies multiply signed commits' relies on the
commit/author date that was incremented via test_tick in the inital function
doing all the commits even though it creates its own. This should be reset
or otherwise set to a known state. But I'm not sure how.

Fabian Stelzer (9):
  ssh signing: preliminary refactoring and clean-up
  ssh signing: add test prereqs
  ssh signing: add ssh key format and signing code
  ssh signing: retrieve a default key from ssh-agent
  ssh signing: provide a textual signing_key_id
  ssh signing: verify signatures using ssh-keygen
  ssh signing: duplicate t7510 tests for commits
  ssh signing: tests for logs, tags & push certs
  ssh signing: test that gpg fails for unkown keys

 Documentation/config/gpg.txt     |  45 ++-
 Documentation/config/user.txt    |   7 +
 builtin/receive-pack.c           |   4 +
 fmt-merge-msg.c                  |   6 +-
 gpg-interface.c                  | 571 ++++++++++++++++++++++++++++---
 gpg-interface.h                  |   8 +-
 log-tree.c                       |   8 +-
 pretty.c                         |   4 +-
 send-pack.c                      |   8 +-
 t/lib-gpg.sh                     |  28 ++
 t/t4202-log.sh                   |  23 ++
 t/t5534-push-signed.sh           | 101 ++++++
 t/t7031-verify-tag-signed-ssh.sh | 161 +++++++++
 t/t7510-signed-commit.sh         |  29 +-
 t/t7528-signed-commit-ssh.sh     | 398 +++++++++++++++++++++
 15 files changed, 1335 insertions(+), 66 deletions(-)
 create mode 100755 t/t7031-verify-tag-signed-ssh.sh
 create mode 100755 t/t7528-signed-commit-ssh.sh


base-commit: 940fe202adcbf9fa1825c648d97cbe1b90d26aec
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1041%2FFStelzer%2Fsshsign-v7
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1041/FStelzer/sshsign-v7
Pull-Request: https://github.com/git/git/pull/1041

Range-diff vs v6:

  1:  7c8502c65b8 !  1:  91fd0159e1f ssh signing: preliminary refactoring and clean-up
     @@ gpg-interface.c: static int verify_signed_buffer(const char *payload, size_t pay
      -	parse_gpg_output(sigc);
      +	fmt = get_format_by_sig(signature);
      +	if (!fmt)
     -+		return error(_("bad/incompatible signature '%s'"), signature);
     ++		die(_("bad/incompatible signature '%s'"), signature);
      +
      +	status = fmt->verify_signed_buffer(sigc, fmt, payload, plen, signature,
      +					   slen);
     @@ gpg-interface.c: const char *get_signing_key(void)
      +}
      +
      +static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
     -+		    const char *signing_key)
     ++			  const char *signing_key)
       {
       	struct child_process gpg = CHILD_PROCESS_INIT;
       	int ret;
  6:  18a26ca49e7 !  2:  fe98052a3ea ssh signing: add test prereqs
     @@ Metadata
       ## Commit message ##
          ssh signing: add test prereqs
      
     -    generate some ssh keys and a allowedSignersFile for testing
     +    Generate some ssh keys and a allowedSignersFile for testing
      
          Signed-off-by: Fabian Stelzer [off-list ref]
      
     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
       	echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null
       '
       
     ++GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key"
     ++GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key"
     ++GPGSSH_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key"
     ++GPGSSH_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key"
     ++GPGSSH_KEY_PASSPHRASE="super_secret"
     ++GPGSSH_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile"
     ++
     ++GPGSSH_GOOD_SIGNATURE_TRUSTED='Good "git" signature for'
     ++GPGSSH_GOOD_SIGNATURE_UNTRUSTED='Good "git" signature with'
     ++GPGSSH_KEY_NOT_TRUSTED="No principal matched"
     ++GPGSSH_BAD_SIGNATURE="Signature verification failed"
     ++
      +test_lazy_prereq GPGSSH '
      +	ssh_version=$(ssh-keygen -Y find-principals -n "git" 2>&1)
      +	test $? != 127 || exit 1
     @@ t/lib-gpg.sh: test_lazy_prereq RFC1991 '
      +	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
     ++	ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null &&
     ++	echo "\"principal with number 1\" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
     ++	ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null &&
     ++	echo "\"principal with number 2\" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
     ++	ssh-keygen -t ed25519 -N "${GPGSSH_KEY_PASSPHRASE}" -C "git ed25519 encrypted key" -f "${GPGSSH_KEY_WITH_PASSPHRASE}" >/dev/null &&
     ++	echo "\"principal with number 3\" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
     ++	ssh-keygen -t ed25519 -N "" -f "${GPGSSH_KEY_UNTRUSTED}" >/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 '
  2:  f05bab16096 !  3:  80d2d55d22e ssh signing: add ssh signature format and signing using ssh keys
     @@ Metadata
      Author: Fabian Stelzer [off-list ref]
      
       ## Commit message ##
     -    ssh signing: add ssh signature format and signing using ssh keys
     +    ssh signing: add ssh key format and signing code
      
     -    implements the actual sign_buffer_ssh operation and move some shared
     +    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
     @@ Commit message
      
          Signed-off-by: Fabian Stelzer [off-list ref]
      
     + ## 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/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.
     +
       ## gpg-interface.c ##
      @@ gpg-interface.c: static const char *x509_sigs[] = {
       	NULL
     @@ gpg-interface.c: int sign_buffer(struct strbuf *buffer, struct strbuf *signature
      +}
      +
       static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
     - 		    const char *signing_key)
     + 			  const char *signing_key)
       {
       	struct child_process gpg = CHILD_PROCESS_INIT;
       	int ret;
     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf
      +				    key_file->filename.buf);
      +			goto out;
      +		}
     -+		ssh_signing_key_file = key_file->filename.buf;
     ++		ssh_signing_key_file = strbuf_detach(&key_file->filename, NULL);
      +	} else {
      +		/* We assume a file */
      +		ssh_signing_key_file = expand_user_path(signing_key, 1);
     @@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf
      +		delete_tempfile(&buffer_file);
      +	strbuf_release(&signer_stderr);
      +	strbuf_release(&ssh_signature_filename);
     ++	FREE_AND_NULL(ssh_signing_key_file);
      +	return ret;
      +}
  3:  071e6173d8e !  4:  83ece42e1de ssh signing: retrieve a default key from ssh-agent
     @@ Metadata
       ## Commit message ##
          ssh signing: retrieve a default key from ssh-agent
      
     -    if user.signingkey is not set and a ssh signature is requested we call
     -    ssh-add -L and use the first key we get
     +    If user.signingkey is not set and a ssh signature is requested we call
     +    gpg.ssh.defaultKeyCommand (typically "ssh-add -L") and use the first key we get
      
          Signed-off-by: Fabian Stelzer [off-list ref]
      
     + ## Documentation/config/gpg.txt ##
     +@@ Documentation/config/gpg.txt: gpg.minTrustLevel::
     + * `marginal`
     + * `fully`
     + * `ultimate`
     ++
     ++gpg.ssh.defaultKeyCommand:
     ++	This command that will be run when user.signingkey is not set and a ssh
     ++	signature is requested. On successful exit a valid ssh public key is
     ++	expected in the	first line of its output. To automatically use the first
     ++	available key from your ssh-agent set this to "ssh-add -L".
     +
     + ## Documentation/config/user.txt ##
     +@@ Documentation/config/user.txt: user.signingKey::
     + 	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.
     ++	a file containing a private key directly. If not set git will call
     ++	gpg.ssh.defaultKeyCommand (e.g.: "ssh-add -L") and try to use the first
     ++	key available.
     +
       ## gpg-interface.c ##
     +@@
     + #include "gpg-interface.h"
     + #include "sigchain.h"
     + #include "tempfile.h"
     ++#include "alias.h"
     + 
     + static char *configured_signing_key;
     ++static const char *ssh_default_key_command;
     + static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
     + 
     + struct gpg_format {
     +@@ gpg-interface.c: struct gpg_format {
     + 				    size_t signature_size);
     + 	int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,
     + 			   const char *signing_key);
     ++	const char *(*get_default_key)(void);
     + };
     + 
     + static const char *openpgp_verify_args[] = {
     +@@ gpg-interface.c: static int sign_buffer_gpg(struct strbuf *buffer, struct strbuf *signature,
     + static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
     + 			   const char *signing_key);
     + 
     ++static const char *get_default_ssh_signing_key(void);
     ++
     + static struct gpg_format gpg_format[] = {
     + 	{
     + 		.name = "openpgp",
     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
     + 		.sigs = openpgp_sigs,
     + 		.verify_signed_buffer = verify_gpg_signed_buffer,
     + 		.sign_buffer = sign_buffer_gpg,
     ++		.get_default_key = NULL,
     + 	},
     + 	{
     + 		.name = "x509",
     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
     + 		.sigs = x509_sigs,
     + 		.verify_signed_buffer = verify_gpg_signed_buffer,
     + 		.sign_buffer = sign_buffer_gpg,
     ++		.get_default_key = NULL,
     + 	},
     + 	{
     + 		.name = "ssh",
     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
     + 		.verify_args = ssh_verify_args,
     + 		.sigs = ssh_sigs,
     + 		.verify_signed_buffer = NULL, /* TODO */
     +-		.sign_buffer = sign_buffer_ssh
     ++		.sign_buffer = sign_buffer_ssh,
     ++		.get_default_key = get_default_ssh_signing_key,
     + 	},
     + };
     + 
     +@@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
     + 		return 0;
     + 	}
     + 
     ++	if (!strcmp(var, "gpg.ssh.defaultkeycommand")) {
     ++		if (!value)
     ++			return config_error_nonbool(var);
     ++		return git_config_string(&ssh_default_key_command, var, value);
     ++	}
     ++
     + 	if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
     + 		fmtname = "openpgp";
     + 
      @@ gpg-interface.c: 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)
     ++static const char *get_default_ssh_signing_key(void)
      +{
     -+	struct child_process ssh_add = CHILD_PROCESS_INIT;
     ++	struct child_process ssh_default_key = CHILD_PROCESS_INIT;
      +	int ret = -1;
     -+	struct strbuf key_stdout = STRBUF_INIT;
     ++	struct strbuf key_stdout = STRBUF_INIT, key_stderr = STRBUF_INIT;
      +	struct strbuf **keys;
     ++	char *key_command = NULL;
     ++	const char **argv;
     ++	int n;
     ++	char *default_key = NULL;
     ++
     ++	if (!ssh_default_key_command)
     ++		die(_("either user.signingkey or gpg.ssh.defaultKeyCommand needs to be configured"));
     ++
     ++	key_command = xstrdup(ssh_default_key_command);
     ++	n = split_cmdline(key_command, &argv);
     ++
     ++	if (n < 0)
     ++		die("malformed build-time gpg.ssh.defaultKeyCommand: %s",
     ++		    split_cmdline_strerror(n));
     ++
     ++	strvec_pushv(&ssh_default_key.args, argv);
     ++	ret = pipe_command(&ssh_default_key, NULL, 0, &key_stdout, 0,
     ++			   &key_stderr, 0);
      +
     -+	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);
     ++		if (keys[0] && starts_with(keys[0]->buf, "ssh-")) {
     ++			default_key = strbuf_detach(keys[0], NULL);
     ++		} else {
     ++			warning(_("gpg.ssh.defaultKeycommand succeeded but returned no keys: %s %s"),
     ++				key_stderr.buf, key_stdout.buf);
     ++		}
     ++
     ++		strbuf_list_free(keys);
     ++	} else {
     ++		warning(_("gpg.ssh.defaultKeyCommand failed: %s %s"),
     ++			key_stderr.buf, key_stdout.buf);
      +	}
      +
     ++	free(key_command);
     ++	free(argv);
      +	strbuf_release(&key_stdout);
     -+	return "";
     ++
     ++	return default_key;
      +}
      +
       const char *get_signing_key(void)
     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
       	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);
     ++	if (use_format->get_default_key) {
     ++		return use_format->get_default_key();
      +	}
     ++
     ++	return git_committer_info(IDENT_STRICT | IDENT_NO_DATE);
       }
       
       int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
  4:  7d1d131ff5b !  5:  76bc9eb4079 ssh signing: provide a textual representation of the signing key
     @@ Metadata
      Author: Fabian Stelzer [off-list ref]
      
       ## Commit message ##
     -    ssh signing: provide a textual representation of the signing key
     +    ssh signing: provide a textual signing_key_id
      
     -    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.
     +    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 [off-list ref]
      
       ## gpg-interface.c ##
     +@@ gpg-interface.c: struct gpg_format {
     + 	int (*sign_buffer)(struct strbuf *buffer, struct strbuf *signature,
     + 			   const char *signing_key);
     + 	const char *(*get_default_key)(void);
     ++	const char *(*get_key_id)(void);
     + };
     + 
     + static const char *openpgp_verify_args[] = {
     +@@ gpg-interface.c: static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,
     + 
     + static const char *get_default_ssh_signing_key(void);
     + 
     ++static const char *get_ssh_key_id(void);
     ++
     + static struct gpg_format gpg_format[] = {
     + 	{
     + 		.name = "openpgp",
     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
     + 		.verify_signed_buffer = verify_gpg_signed_buffer,
     + 		.sign_buffer = sign_buffer_gpg,
     + 		.get_default_key = NULL,
     ++		.get_key_id = NULL,
     + 	},
     + 	{
     + 		.name = "x509",
     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
     + 		.verify_signed_buffer = verify_gpg_signed_buffer,
     + 		.sign_buffer = sign_buffer_gpg,
     + 		.get_default_key = NULL,
     ++		.get_key_id = NULL,
     + 	},
     + 	{
     + 		.name = "ssh",
     +@@ gpg-interface.c: static struct gpg_format gpg_format[] = {
     + 		.verify_signed_buffer = NULL, /* TODO */
     + 		.sign_buffer = sign_buffer_ssh,
     + 		.get_default_key = get_default_ssh_signing_key,
     ++		.get_key_id = get_ssh_key_id,
     + 	},
     + };
     + 
      @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
       	return 0;
       }
     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
      +	 * 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-")) {
     ++	if (starts_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,
     @@ 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 const char *get_default_ssh_signing_key(void)
       {
     -@@ gpg-interface.c: static char *get_default_ssh_signing_key(void)
     - 	return "";
     +@@ gpg-interface.c: static const char *get_default_ssh_signing_key(void)
     + 	return default_key;
       }
       
     -+/* Returns a textual but unique representation ot the signing key */
     ++static const char *get_ssh_key_id(void) {
     ++	return get_ssh_key_fingerprint(get_signing_key());
     ++}
     ++
     ++/* Returns a textual but unique representation of 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();
     ++	if (use_format->get_key_id) {
     ++		return use_format->get_key_id();
      +	}
     ++
     ++	/* GPG/GPGSM only store a key id on this variable */
     ++	return get_signing_key();
      +}
      +
       const char *get_signing_key(void)
  5:  725764018ce !  6:  dc092c79796 ssh signing: parse ssh-keygen output and verify signatures
     @@ Metadata
      Author: Fabian Stelzer [off-list ref]
      
       ## Commit message ##
     -    ssh signing: parse ssh-keygen output and verify signatures
     +    ssh signing: verify signatures using ssh-keygen
      
     -    to verify a ssh signature we first call ssh-keygen -Y find-principal to
     +    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.
     @@ Commit message
          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.
     +    repository server from known ssh keys with push access. 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.
      
          To revoke a key put the public key without the principal prefix into
          gpg.ssh.revocationKeyring or generate a KRL (see ssh-keygen(1)
     @@ Commit message
          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.
     +    See "CERTIFICATES" in ssh-keygen(1).
      
          Signed-off-by: Fabian Stelzer [off-list ref]
      
     + ## Documentation/config/gpg.txt ##
     +@@ Documentation/config/gpg.txt: gpg.ssh.defaultKeyCommand:
     + 	signature is requested. On successful exit a valid ssh public key is
     + 	expected in the	first line of its output. To automatically use the first
     + 	available key from your ssh-agent set this to "ssh-add -L".
     ++
     ++gpg.ssh.allowedSignersFile::
     ++	A file containing ssh public keys which you are willing to trust.
     ++	The file consists of one or more lines of principals followed by an ssh
     ++	public key.
     ++	e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...
     ++	See ssh-keygen(1) "ALLOWED SIGNERS" for details.
     ++	The principal is only used to identify the key and is available when
     ++	verifying a signature.
     +++
     ++SSH has no concept of trust levels like gpg does. To be able to differentiate
     ++between valid signatures and trusted signatures the trust level of a signature
     ++verification is set to `fully` when the public key is present in the allowedSignersFile.
     ++Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.
     ++Otherwise valid but untrusted signatures will still verify but show no principal
     ++name of the signer.
     +++
     ++This file can be set to a location outside of the repository and every developer
     ++maintains their own trust store. A central repository server could generate this
     ++file automatically from ssh keys with push access to verify the code against.
     ++In a corporate setting this file is probably generated at a global location
     ++from automation that already handles developer ssh keys.
     +++
     ++A repository that only allows signed commits can store the file
     ++in the repository itself using a path relative to the top-level of the working tree.
     ++This way only committers with an already valid key can add or change keys in the keyring.
     +++
     ++Using a SSH CA key with the cert-authority option
     ++(see ssh-keygen(1) "CERTIFICATES") is also valid.
     ++
     ++gpg.ssh.revocationFile::
     ++	Either a SSH KRL or a list of revoked public keys (without the principal prefix).
     ++	See ssh-keygen(1) for details.
     ++	If a public key is found in this file then it will always be treated
     ++	as having trust level "never" and signatures will show as invalid.
     +
       ## 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;
      +
     ++	status = git_gpg_config(var, value, NULL);
       	if (status)
       		return status;
       
     @@ gpg-interface.c
       #include "gpg-interface.h"
       #include "sigchain.h"
       #include "tempfile.h"
     + #include "alias.h"
       
       static char *configured_signing_key;
     -+static const char *ssh_allowed_signers, *ssh_revocation_file;
     +-static const char *ssh_default_key_command;
     ++static const char *ssh_default_key_command, *ssh_allowed_signers, *ssh_revocation_file;
       static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
       
       struct gpg_format {
     @@ gpg-interface.c: static struct gpg_format gpg_format[] = {
       		.sigs = ssh_sigs,
      -		.verify_signed_buffer = NULL, /* TODO */
      +		.verify_signed_buffer = verify_ssh_signed_buffer,
     - 		.sign_buffer = sign_buffer_ssh
     - 	},
     - };
     + 		.sign_buffer = sign_buffer_ssh,
     + 		.get_default_key = get_default_ssh_signing_key,
     + 		.get_key_id = get_ssh_key_id,
      @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sigc,
       	return ret;
       }
     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
      +static void parse_ssh_output(struct signature_check *sigc)
      +{
      +	const char *line, *principal, *search;
     ++	char *key = NULL;
      +
      +	/*
     -+	 * ssh-keysign output should be:
     ++	 * ssh-keygen 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
     ++	 *
     ++	 * 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.
      +	 */
      +	sigc->result = 'B';
      +	sigc->trust_level = TRUST_NEVER;
     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
      +				line = search + 1;
      +		} while (search != NULL);
      +		sigc->signer = xmemdupz(principal, line - principal - 1);
     -+		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
     -+		sigc->key = xstrdup(sigc->fingerprint);
      +	} else if (skip_prefix(line, "Good \"git\" signature with ", &line)) {
      +		/* Valid signature, but key unknown */
      +		sigc->result = 'G';
      +		sigc->trust_level = TRUST_UNDEFINED;
     ++	} else {
     ++		return;
     ++	}
     ++
     ++	key = strstr(line, "key");
     ++	if (key) {
      +		sigc->fingerprint = xstrdup(strstr(line, "key") + 4);
      +		sigc->key = xstrdup(sigc->fingerprint);
     ++	} else {
     ++		/*
     ++		 * Output did not match what we expected
     ++		 * Treat the signature as bad
     ++		 */
     ++		sigc->result = 'B';
      +	}
      +}
      +
     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
      +		goto out;
      +	}
      +	if (ret || !ssh_keygen_out.len) {
     -+		/* We did not find a matching principal in the allowedSigners - 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", buffer_file->filename.buf,
      +			     NULL);
     -+		ret = pipe_command(&ssh_keygen, payload, payload_size,
     ++		pipe_command(&ssh_keygen, payload, payload_size,
      +				   &ssh_keygen_out, 0, &ssh_keygen_err, 0);
     ++
     ++		/*
     ++		 * Fail on unknown keys
     ++		 * we still call check-novalidate to display the signature info
     ++		 */
     ++		ret = -1;
      +	} else {
      +		/* Check every principal we found (one per line) */
      +		for (line = ssh_keygen_out.buf; *line;
     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
      +			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 */
     ++			/*
     ++			 * We found principals
     ++			 * Try with each until we find a match
     ++			 */
      +			strvec_pushl(&ssh_keygen.args, "-Y", "verify",
      +				     "-n", "git",
      +				     "-f", ssh_allowed_signers,
     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
      +
      +			FREE_AND_NULL(principal);
      +
     -+			ret &= starts_with(ssh_keygen_out.buf, "Good");
     -+			if (ret == 0)
     ++			if (!ret)
     ++				ret = !starts_with(ssh_keygen_out.buf, "Good");
     ++
     ++			if (!ret)
      +				break;
      +		}
      +	}
     @@ gpg-interface.c: static int verify_gpg_signed_buffer(struct signature_check *sig
       	size_t slen, struct signature_check *sigc)
       {
      @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb)
     - 		return 0;
     + 		return git_config_string(&ssh_default_key_command, var, value);
       	}
       
      +	if (!strcmp(var, "gpg.ssh.allowedsignersfile")) {
     @@ gpg-interface.c: int git_gpg_config(const char *var, const char *value, void *cb
      +		return git_config_string(&ssh_allowed_signers, var, value);
      +	}
      +
     -+	if (!strcmp(var, "gpg.ssh.revocationFile")) {
     ++	if (!strcmp(var, "gpg.ssh.revocationfile")) {
      +		if (!value)
      +			return config_error_nonbool(var);
      +		return git_config_string(&ssh_revocation_file, var, value);
  7:  01da9a07934 !  7:  c17441566d9 ssh signing: duplicate t7510 tests for commits
     @@ t/t7528-signed-commit-ssh.sh (new)
      +
      +	test_when_finished "test_unconfig commit.gpgsign" &&
      +	test_config gpg.format ssh &&
     -+	test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
     ++	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
      +
      +	echo 1 >file && git add file &&
      +	test_tick && git commit -S -m initial &&
     @@ t/t7528-signed-commit-ssh.sh (new)
      +	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}" &&
     ++	echo 8 >file && test_tick && git commit -a -m eighth -S"${GPGSSH_KEY_UNTRUSTED}" &&
      +	git tag eighth-signed-alt &&
      +
      +	# commit.gpgsign is still on but this must not be signed
     @@ t/t7528-signed-commit-ssh.sh (new)
      +	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 &&
     ++	echo 12 | git commit-tree --gpg-sign="${GPGSSH_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.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	test_config gpg.mintrustlevel UNDEFINED &&
      +	(
      +		for commit in initial second merge fourth-signed \
     @@ t/t7528-signed-commit-ssh.sh (new)
      +		do
      +			git verify-commit $commit &&
      +			git show --pretty=short --show-signature $commit >actual &&
     -+			grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
     -+			! grep "${BAD_SIGNATURE}" actual &&
     ++			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $commit OK || exit 1
      +		done
      +	) &&
     @@ t/t7528-signed-commit-ssh.sh (new)
      +		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 &&
     ++			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $commit OK || exit 1
      +		done
      +	) &&
     @@ t/t7528-signed-commit-ssh.sh (new)
      +		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 "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
     ++			! grep "${GPGSSH_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 &&
     ++test_expect_success GPGSSH 'verify-commit exits failure on untrusted signature' '
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
     ++	test_must_fail git verify-commit eighth-signed-alt 2>actual &&
     ++	grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
     ++	! grep "${GPGSSH_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.ssh.allowedSignersFile "${GPGSSH_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.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	test_config gpg.minTrustLevel marginal &&
      +	git verify-commit sixth-signed
      +'
     @@ t/t7528-signed-commit-ssh.sh (new)
      +'
      +
      +test_expect_success GPGSSH 'verify signatures with --raw' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_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 &&
     ++			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $commit OK || exit 1
      +		done
      +	) &&
     @@ t/t7528-signed-commit-ssh.sh (new)
      +		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 &&
     ++			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_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 &&
     ++			test_must_fail git verify-commit --raw $commit 2>actual &&
     ++			grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $commit OK || exit 1
      +		done
      +	)
     @@ t/t7528-signed-commit-ssh.sh (new)
      +'
      +
      +test_expect_success GPGSSH 'show signed commit with signature' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_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 -e "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" -e "Warning: " show >show.commit &&
     ++	grep -e "${GPGSSH_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 &&
     @@ t/t7528-signed-commit-ssh.sh (new)
      +'
      +
      +test_expect_success GPGSSH 'detect fudged signature' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_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
     ++	grep "${GPGSSH_BAD_SIGNATURE}" actual1 &&
     ++	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual1 &&
     ++	! grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual1
      +'
      +
      +test_expect_success GPGSSH 'detect fudged signature with NUL' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_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
     ++	grep "${GPGSSH_BAD_SIGNATURE}" actual2 &&
     ++	! grep "${GPGSSH_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}" &&
     ++	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_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
     ++	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++	! grep "${GPGSSH_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;}") &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
     ++	FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") &&
      +	cat >expect.tmpl <<-\EOF &&
      +	G
      +	FINGERPRINT
     @@ t/t7528-signed-commit-ssh.sh (new)
      +'
      +
      +test_expect_success GPGSSH 'show bad signature with custom format' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	cat >expect <<-\EOF &&
      +	B
      +
     @@ t/t7528-signed-commit-ssh.sh (new)
      +'
      +
      +test_expect_success GPGSSH 'show untrusted signature with custom format' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	cat >expect.tmpl <<-\EOF &&
      +	U
      +	FINGERPRINT
     @@ t/t7528-signed-commit-ssh.sh (new)
      +
      +	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;}") &&
     ++	FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_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}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	cat >expect.tmpl <<-\EOF &&
      +	undefined
      +	FINGERPRINT
     @@ t/t7528-signed-commit-ssh.sh (new)
      +
      +	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;}") &&
     ++	FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_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}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	cat >expect.tmpl <<-\EOF &&
      +	fully
      +	FINGERPRINT
     @@ t/t7528-signed-commit-ssh.sh (new)
      +
      +	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;}") &&
     ++	FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") &&
      +	sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect &&
      +	test_cmp expect actual
      +'
     @@ t/t7528-signed-commit-ssh.sh (new)
      +'
      +
      +test_expect_success GPGSSH 'log.showsignature behaves like --show-signature' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	test_config log.showsignature true &&
      +	git show initial >actual &&
     -+	grep "${GOOD_SIGNATURE_TRUSTED}" actual
     ++	grep "${GPGSSH_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 user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
      +	test_config gpg.format ssh &&
      +	git commit -S --amend -m "success" &&
      +	test_config gpg.format OpEnPgP &&
  8:  d9707443f5c !  8:  0763517d62d ssh signing: add more tests for logs, tags & push certs
     @@ Metadata
      Author: Fabian Stelzer [off-list ref]
      
       ## Commit message ##
     -    ssh signing: add more tests for logs, tags & push certs
     +    ssh signing: tests for logs, tags & push certs
      
          Signed-off-by: Fabian Stelzer [off-list ref]
      
     @@ t/t4202-log.sh: test_expect_success GPGSM 'setup signed branch x509' '
       
      +test_expect_success GPGSSH 'setup sshkey signed branch' '
      +	test_config gpg.format ssh &&
     -+	test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
     ++	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
      +	test_when_finished "git reset --hard && git checkout main" &&
      +	git checkout -b signed-ssh main &&
      +	echo foo >foo &&
     @@ t/t4202-log.sh: test_expect_success GPGSM 'log OpenPGP fingerprint' '
       '
       
      +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 &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
     ++	ssh-keygen -lf  "${GPGSSH_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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	git -C dst config gpg.ssh.allowedSignersFile "${GPGSSH_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
      +	EOF
      +
      +	test_config gpg.format ssh &&
     -+	test_config user.signingkey "${SIGNING_KEY_PRIMARY}" &&
     -+	FINGERPRINT=$(ssh-keygen -lf "${SIGNING_KEY_PRIMARY}" | awk "{print \$2;}") &&
     ++	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
     ++	FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") &&
      +	git push --signed dst noop ff +noff &&
      +
      +	(
     @@ 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.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	git -C dst config gpg.ssh.allowedSignersFile "${GPGSSH_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
      +		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;}") &&
     ++	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
     ++	FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") &&
      +	git push --signed dst noop ff +noff &&
      +
      +	(
     @@ t/t7031-verify-tag-signed-ssh.sh (new)
      +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}" &&
     ++	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
      +
      +	echo 1 >file && git add file &&
      +	test_tick && git commit -m initial &&
     @@ t/t7031-verify-tag-signed-ssh.sh (new)
      +	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
     ++	git tag -u"${GPGSSH_KEY_UNTRUSTED}" -m eighth eighth-signed-alt
      +'
      +
      +test_expect_success GPGSSH 'verify and show ssh signatures' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	(
      +		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 &&
     ++			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $tag OK || exit 1
      +		done
      +	) &&
     @@ t/t7031-verify-tag-signed-ssh.sh (new)
      +		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 &&
     ++			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_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 &&
     ++			test_must_fail git verify-tag $tag 2>actual &&
     ++			grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
     ++			grep "${GPGSSH_KEY_NOT_TRUSTED}" actual &&
      +			echo $tag OK || exit 1
      +		done
      +	)
      +'
      +
      +test_expect_success GPGSSH 'detect fudged ssh signature' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_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 &&
      +	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
     ++	grep "${GPGSSH_BAD_SIGNATURE}" actual1 &&
     ++	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual1 &&
     ++	! grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual1
      +'
      +
      +test_expect_success GPGSSH 'verify ssh signatures with --raw' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	(
      +		for tag in initial second merge fourth-signed sixth-signed seventh-signed
      +		do
      +			git verify-tag --raw $tag 2>actual &&
     -+			grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
     -+			! grep "${BAD_SIGNATURE}" actual &&
     ++			grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $tag OK || exit 1
      +		done
      +	) &&
     @@ t/t7031-verify-tag-signed-ssh.sh (new)
      +		for tag in fourth-unsigned fifth-unsigned sixth-unsigned
      +		do
      +			test_must_fail git verify-tag --raw $tag 2>actual &&
     -+			! grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
     -+			! grep "${BAD_SIGNATURE}" actual &&
     ++			! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $tag OK || exit 1
      +		done
      +	) &&
      +	(
      +		for tag in eighth-signed-alt
      +		do
     -+			git verify-tag --raw $tag 2>actual &&
     -+			grep "${GOOD_SIGNATURE_UNTRUSTED}" actual &&
     -+			! grep "${BAD_SIGNATURE}" actual &&
     ++			test_must_fail git verify-tag --raw $tag 2>actual &&
     ++			grep "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
     ++			! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +			echo $tag OK || exit 1
      +		done
      +	)
      +'
      +
      +test_expect_success GPGSSH 'verify signatures with --raw ssh' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	git verify-tag --raw sixth-signed 2>actual &&
     -+	grep "${GOOD_SIGNATURE_TRUSTED}" actual &&
     -+	! grep "${BAD_SIGNATURE}" actual &&
     ++	grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
     ++	! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
      +	echo sixth-signed OK
      +'
      +
      +test_expect_success GPGSSH 'verify multiple tags ssh' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	tags="seventh-signed sixth-signed" &&
      +	for i in $tags
      +	do
      +		git verify-tag -v --raw $i || return 1
      +	done >expect.stdout 2>expect.stderr.1 &&
     -+	grep "^${GOOD_SIGNATURE_TRUSTED}" <expect.stderr.1 >expect.stderr &&
     ++	grep "^${GPGSSH_GOOD_SIGNATURE_TRUSTED}" <expect.stderr.1 >expect.stderr &&
      +	git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
     -+	grep "^${GOOD_SIGNATURE_TRUSTED}" <actual.stderr.1 >actual.stderr &&
     ++	grep "^${GPGSSH_GOOD_SIGNATURE_TRUSTED}" <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 - ssh' '
     -+	test_config gpg.ssh.allowedSignersFile "${SIGNING_ALLOWED_SIGNERS}" &&
     ++	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
      +	cat >expect <<-\EOF &&
      +	tagname : fourth-signed
      +	EOF
  9:  275af516eba <  -:  ----------- ssh signing: add documentation
  -:  ----------- >  9:  a5add98197a ssh signing: test that gpg fails for unkown keys

-- 
gitgitgadget

[PATCH v7 2/9] ssh signing: add test prereqs

From: Fabian Stelzer via GitGitGadget <hidden>
Date: 2021-08-03 13:46:15

From: Fabian Stelzer <redacted>

Generate some ssh keys and a allowedSignersFile for testing

Signed-off-by: Fabian Stelzer <redacted>
---
 t/lib-gpg.sh | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh
index 9fc5241228e..f99ef3e859d 100644
--- a/t/lib-gpg.sh
+++ b/t/lib-gpg.sh
@@ -87,6 +87,34 @@ test_lazy_prereq RFC1991 '
 	echo | gpg --homedir "${GNUPGHOME}" -b --rfc1991 >/dev/null
 '
 
+GPGSSH_KEY_PRIMARY="${GNUPGHOME}/ed25519_ssh_signing_key"
+GPGSSH_KEY_SECONDARY="${GNUPGHOME}/rsa_2048_ssh_signing_key"
+GPGSSH_KEY_UNTRUSTED="${GNUPGHOME}/untrusted_ssh_signing_key"
+GPGSSH_KEY_WITH_PASSPHRASE="${GNUPGHOME}/protected_ssh_signing_key"
+GPGSSH_KEY_PASSPHRASE="super_secret"
+GPGSSH_ALLOWED_SIGNERS="${GNUPGHOME}/ssh.all_valid.allowedSignersFile"
+
+GPGSSH_GOOD_SIGNATURE_TRUSTED='Good "git" signature for'
+GPGSSH_GOOD_SIGNATURE_UNTRUSTED='Good "git" signature with'
+GPGSSH_KEY_NOT_TRUSTED="No principal matched"
+GPGSSH_BAD_SIGNATURE="Signature verification failed"
+
+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 "${GPGSSH_KEY_PRIMARY}" >/dev/null &&
+	echo "\"principal with number 1\" $(cat "${GPGSSH_KEY_PRIMARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
+	ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null &&
+	echo "\"principal with number 2\" $(cat "${GPGSSH_KEY_SECONDARY}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
+	ssh-keygen -t ed25519 -N "${GPGSSH_KEY_PASSPHRASE}" -C "git ed25519 encrypted key" -f "${GPGSSH_KEY_WITH_PASSPHRASE}" >/dev/null &&
+	echo "\"principal with number 3\" $(cat "${GPGSSH_KEY_WITH_PASSPHRASE}.pub")" >> "${GPGSSH_ALLOWED_SIGNERS}" &&
+	ssh-keygen -t ed25519 -N "" -f "${GPGSSH_KEY_UNTRUSTED}" >/dev/null
+'
+
 sanitize_pgp() {
 	perl -ne '
 		/^-----END PGP/ and $in_pgp = 0;
-- 
gitgitgadget
Next 22 of 22 remaining
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help