Merge branch 'fs/ssh-signing'
Use ssh public crypto for object and push-cert signing. * fs/ssh-signing: ssh signing: test that gpg fails for unknown keys ssh signing: tests for logs, tags & push certs ssh signing: duplicate t7510 tests for commits ssh signing: verify signatures using ssh-keygen ssh signing: provide a textual signing_key_id ssh signing: retrieve a default key from ssh-agent ssh signing: add ssh key format and signing code ssh signing: add test prereqs ssh signing: preliminary refactoring and clean-up
This commit is contained in:
commit
18c6653da0
@ -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,44 @@ 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".
|
||||
|
||||
gpg.ssh.allowedSignersFile::
|
||||
A file containing ssh public keys which you are willing to trust.
|
||||
The file consists of one or more lines of principals followed by an ssh
|
||||
public key.
|
||||
e.g.: user1@example.com,user2@example.com ssh-rsa AAAAX1...
|
||||
See ssh-keygen(1) "ALLOWED SIGNERS" for details.
|
||||
The principal is only used to identify the key and is available when
|
||||
verifying a signature.
|
||||
+
|
||||
SSH has no concept of trust levels like gpg does. To be able to differentiate
|
||||
between valid signatures and trusted signatures the trust level of a signature
|
||||
verification is set to `fully` when the public key is present in the allowedSignersFile.
|
||||
Therefore to only mark fully trusted keys as verified set gpg.minTrustLevel to `fully`.
|
||||
Otherwise valid but untrusted signatures will still verify but show no principal
|
||||
name of the signer.
|
||||
+
|
||||
This file can be set to a location outside of the repository and every developer
|
||||
maintains their own trust store. A central repository server could generate this
|
||||
file automatically from ssh keys with push access to verify the code against.
|
||||
In a corporate setting this file is probably generated at a global location
|
||||
from automation that already handles developer ssh keys.
|
||||
+
|
||||
A repository that only allows signed commits can store the file
|
||||
in the repository itself using a path relative to the top-level of the working tree.
|
||||
This way only committers with an already valid key can add or change keys in the keyring.
|
||||
+
|
||||
Using a SSH CA key with the cert-authority option
|
||||
(see ssh-keygen(1) "CERTIFICATES") is also valid.
|
||||
|
||||
gpg.ssh.revocationFile::
|
||||
Either a SSH KRL or a list of revoked public keys (without the principal prefix).
|
||||
See ssh-keygen(1) for details.
|
||||
If a public key is found in this file then it will always be treated
|
||||
as having trust level "never" and signatures will show as invalid.
|
||||
|
@ -36,3 +36,10 @@ 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
|
||||
gpg.ssh.defaultKeyCommand (e.g.: "ssh-add -L") and try to use the first
|
||||
key available.
|
||||
|
@ -132,6 +132,10 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
|
||||
{
|
||||
int status = parse_hide_refs_config(var, value, "receive");
|
||||
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
status = git_gpg_config(var, value, NULL);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
|
@ -528,11 +528,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);
|
||||
|
||||
|
577
gpg-interface.c
577
gpg-interface.c
@ -3,11 +3,14 @@
|
||||
#include "config.h"
|
||||
#include "run-command.h"
|
||||
#include "strbuf.h"
|
||||
#include "dir.h"
|
||||
#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, *ssh_allowed_signers, *ssh_revocation_file;
|
||||
static enum signature_trust_level configured_min_trust_level = TRUST_UNDEFINED;
|
||||
|
||||
struct gpg_format {
|
||||
@ -15,6 +18,14 @@ 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);
|
||||
const char *(*get_default_key)(void);
|
||||
const char *(*get_key_id)(void);
|
||||
};
|
||||
|
||||
static const char *openpgp_verify_args[] = {
|
||||
@ -35,14 +46,59 @@ 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 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,
|
||||
const char *signing_key);
|
||||
|
||||
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", .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,
|
||||
.get_default_key = NULL,
|
||||
.get_key_id = NULL,
|
||||
},
|
||||
{ .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,
|
||||
.get_default_key = NULL,
|
||||
.get_key_id = NULL,
|
||||
},
|
||||
{
|
||||
.name = "ssh",
|
||||
.program = "ssh-keygen",
|
||||
.verify_args = ssh_verify_args,
|
||||
.sigs = ssh_sigs,
|
||||
.verify_signed_buffer = verify_ssh_signed_buffer,
|
||||
.sign_buffer = sign_buffer_ssh,
|
||||
.get_default_key = get_default_ssh_signing_key,
|
||||
.get_key_id = get_ssh_key_id,
|
||||
},
|
||||
};
|
||||
|
||||
@ -72,7 +128,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 +313,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 +335,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 +342,216 @@ 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;
|
||||
}
|
||||
|
||||
static void parse_ssh_output(struct signature_check *sigc)
|
||||
{
|
||||
const char *line, *principal, *search;
|
||||
char *key = NULL;
|
||||
|
||||
/*
|
||||
* ssh-keygen output should be:
|
||||
* Good "git" signature for PRINCIPAL with RSA key SHA256:FINGERPRINT
|
||||
*
|
||||
* or for valid but unknown keys:
|
||||
* Good "git" signature with RSA key SHA256:FINGERPRINT
|
||||
*
|
||||
* Note that "PRINCIPAL" can contain whitespace, "RSA" and
|
||||
* "SHA256" part could be a different token that names of
|
||||
* the algorithms used, and "FINGERPRINT" is a hexadecimal
|
||||
* string. By finding the last occurence of " with ", we can
|
||||
* reliably parse out the PRINCIPAL.
|
||||
*/
|
||||
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);
|
||||
} 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';
|
||||
}
|
||||
}
|
||||
|
||||
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_principals_out = STRBUF_INIT;
|
||||
struct strbuf ssh_principals_err = STRBUF_INIT;
|
||||
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_principals_out, 0,
|
||||
&ssh_principals_err, 0);
|
||||
if (ret && strstr(ssh_principals_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_principals_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);
|
||||
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_principals_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);
|
||||
|
||||
if (!ret)
|
||||
ret = !starts_with(ssh_keygen_out.buf, "Good");
|
||||
|
||||
if (!ret)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sigc->payload = xmemdupz(payload, payload_size);
|
||||
strbuf_stripspace(&ssh_keygen_out, 0);
|
||||
strbuf_stripspace(&ssh_keygen_err, 0);
|
||||
/* Add stderr outputs to show the user actual ssh-keygen errors */
|
||||
strbuf_add(&ssh_keygen_out, ssh_principals_err.buf, ssh_principals_err.len);
|
||||
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_principals_out);
|
||||
strbuf_release(&ssh_principals_err);
|
||||
strbuf_release(&ssh_keygen_out);
|
||||
strbuf_release(&ssh_keygen_err);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@ -309,35 +559,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);
|
||||
@ -419,12 +666,33 @@ 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.ssh.allowedsignersfile")) {
|
||||
if (!value)
|
||||
return config_error_nonbool(var);
|
||||
return git_config_pathname(&ssh_allowed_signers, var, value);
|
||||
}
|
||||
|
||||
if (!strcmp(var, "gpg.ssh.revocationfile")) {
|
||||
if (!value)
|
||||
return config_error_nonbool(var);
|
||||
return git_config_pathname(&ssh_revocation_file, var, value);
|
||||
}
|
||||
|
||||
if (!strcmp(var, "gpg.program") || !strcmp(var, "gpg.openpgp.program"))
|
||||
fmtname = "openpgp";
|
||||
|
||||
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,18 +701,144 @@ 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 (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,
|
||||
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 const char *get_default_ssh_signing_key(void)
|
||||
{
|
||||
struct child_process ssh_default_key = CHILD_PROCESS_INIT;
|
||||
int ret = -1;
|
||||
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);
|
||||
|
||||
if (!ret) {
|
||||
keys = strbuf_split_max(&key_stdout, '\n', 2);
|
||||
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 default_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 (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)
|
||||
{
|
||||
if (configured_signing_key)
|
||||
return configured_signing_key;
|
||||
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)
|
||||
{
|
||||
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,
|
||||
@ -470,13 +864,98 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
|
||||
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 = strbuf_detach(&key_file->filename, NULL);
|
||||
} 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);
|
||||
FREE_AND_NULL(ssh_signing_key_file);
|
||||
return ret;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ enum signature_trust_level {
|
||||
|
||||
struct signature_check {
|
||||
char *payload;
|
||||
char *gpg_output;
|
||||
char *output;
|
||||
char *gpg_status;
|
||||
|
||||
/*
|
||||
@ -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);
|
||||
|
@ -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);
|
||||
|
4
pretty.c
4
pretty.c
@ -1436,8 +1436,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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
28
t/lib-gpg.sh
28
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;
|
||||
|
@ -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 "${GPGSSH_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 "${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
|
||||
'
|
||||
|
||||
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 "${GPGSSH_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 &&
|
||||
|
@ -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 "${GPGSSH_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 "${GPGSSH_KEY_PRIMARY}" &&
|
||||
FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_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 "${GPGSSH_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 "${GPGSSH_KEY_PRIMARY}" &&
|
||||
FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_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 &&
|
||||
|
161
t/t7031-verify-tag-signed-ssh.sh
Executable file
161
t/t7031-verify-tag-signed-ssh.sh
Executable file
@ -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 "${GPGSSH_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"${GPGSSH_KEY_UNTRUSTED}" -m eighth eighth-signed-alt
|
||||
'
|
||||
|
||||
test_expect_success GPGSSH 'verify and show ssh signatures' '
|
||||
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 "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_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 "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
|
||||
echo $tag OK || exit 1
|
||||
done
|
||||
) &&
|
||||
(
|
||||
for tag in eighth-signed-alt
|
||||
do
|
||||
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 "${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 "${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 "${GPGSSH_ALLOWED_SIGNERS}" &&
|
||||
(
|
||||
for tag in initial second merge fourth-signed sixth-signed seventh-signed
|
||||
do
|
||||
git verify-tag --raw $tag 2>actual &&
|
||||
grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_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 "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
|
||||
echo $tag OK || exit 1
|
||||
done
|
||||
) &&
|
||||
(
|
||||
for tag in eighth-signed-alt
|
||||
do
|
||||
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 "${GPGSSH_ALLOWED_SIGNERS}" &&
|
||||
git verify-tag --raw sixth-signed 2>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 "${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 "^${GPGSSH_GOOD_SIGNATURE_TRUSTED}" <expect.stderr.1 >expect.stderr &&
|
||||
git verify-tag -v --raw $tags >actual.stdout 2>actual.stderr.1 &&
|
||||
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 "${GPGSSH_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
|
@ -71,7 +71,25 @@ test_expect_success GPG 'create signed commits' '
|
||||
git tag eleventh-signed $(cat oid) &&
|
||||
echo 12 | git commit-tree --gpg-sign=B7227189 HEAD^{tree} >oid &&
|
||||
test_line_count = 1 oid &&
|
||||
git tag twelfth-signed-alt $(cat oid)
|
||||
git tag twelfth-signed-alt $(cat oid) &&
|
||||
|
||||
cat >keydetails <<-\EOF &&
|
||||
Key-Type: RSA
|
||||
Key-Length: 2048
|
||||
Subkey-Type: RSA
|
||||
Subkey-Length: 2048
|
||||
Name-Real: Unknown User
|
||||
Name-Email: unknown@git.com
|
||||
Expire-Date: 0
|
||||
%no-ask-passphrase
|
||||
%no-protection
|
||||
EOF
|
||||
gpg --batch --gen-key keydetails &&
|
||||
echo 13 >file && git commit -a -S"unknown@git.com" -m thirteenth &&
|
||||
git tag thirteenth-signed &&
|
||||
DELETE_FINGERPRINT=$(gpg -K --with-colons --fingerprint --batch unknown@git.com | grep "^fpr" | head -n 1 | awk -F ":" "{print \$10;}") &&
|
||||
gpg --batch --yes --delete-secret-keys $DELETE_FINGERPRINT &&
|
||||
gpg --batch --yes --delete-keys unknown@git.com
|
||||
'
|
||||
|
||||
test_expect_success GPG 'verify and show signatures' '
|
||||
@ -110,6 +128,13 @@ test_expect_success GPG 'verify and show signatures' '
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success GPG 'verify-commit exits failure on unknown signature' '
|
||||
test_must_fail git verify-commit thirteenth-signed 2>actual &&
|
||||
! grep "Good signature from" actual &&
|
||||
! grep "BAD signature from" actual &&
|
||||
grep -q -F -e "No public key" -e "public key not found" actual
|
||||
'
|
||||
|
||||
test_expect_success GPG 'verify-commit exits success on untrusted signature' '
|
||||
git verify-commit eighth-signed-alt 2>actual &&
|
||||
grep "Good signature from" actual &&
|
||||
@ -338,6 +363,8 @@ test_expect_success GPG 'show double signature with custom format' '
|
||||
'
|
||||
|
||||
|
||||
# NEEDSWORK: This test relies on the test_tick commit/author dates from the first
|
||||
# 'create signed commits' test even though it creates its own
|
||||
test_expect_success GPG 'verify-commit verifies multiply signed commits' '
|
||||
git init multiply-signed &&
|
||||
cd multiply-signed &&
|
||||
|
398
t/t7528-signed-commit-ssh.sh
Executable file
398
t/t7528-signed-commit-ssh.sh
Executable file
@ -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 "${GPGSSH_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"${GPGSSH_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="${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 "${GPGSSH_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 "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_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 "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_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 "${GPGSSH_GOOD_SIGNATURE_UNTRUSTED}" actual &&
|
||||
! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
|
||||
grep "${GPGSSH_KEY_NOT_TRUSTED}" actual &&
|
||||
echo $commit OK || exit 1
|
||||
done
|
||||
)
|
||||
'
|
||||
|
||||
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 "${GPGSSH_KEY_NOT_TRUSTED}" actual
|
||||
'
|
||||
|
||||
test_expect_success GPGSSH 'verify-commit exits success with matching minTrustLevel' '
|
||||
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 "${GPGSSH_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 "${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 "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_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 "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual &&
|
||||
! grep "${GPGSSH_BAD_SIGNATURE}" actual &&
|
||||
echo $commit OK || exit 1
|
||||
done
|
||||
) &&
|
||||
(
|
||||
for commit in eighth-signed-alt
|
||||
do
|
||||
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
|
||||
)
|
||||
'
|
||||
|
||||
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 "${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 "${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 &&
|
||||
test_cmp cat.commit verify.1
|
||||
'
|
||||
|
||||
test_expect_success GPGSSH 'detect fudged signature' '
|
||||
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 "${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 "${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 "${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 "${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 "${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 "${GPGSSH_ALLOWED_SIGNERS}" &&
|
||||
FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_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 "${GPGSSH_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 "${GPGSSH_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 "${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 "${GPGSSH_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 "${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 "${GPGSSH_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 "${GPGSSH_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 "${GPGSSH_ALLOWED_SIGNERS}" &&
|
||||
test_config log.showsignature true &&
|
||||
git show initial >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 "${GPGSSH_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
|
Loading…
Reference in New Issue
Block a user