@@ -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;
@@ -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";