receive-pack: GPG-validate push certificates

Reusing the GPG signature check helpers we already have, verify
the signature in receive-pack and give the results to the hooks
via GIT_PUSH_CERT_{SIGNER,KEY,STATUS} environment variables.

Policy decisions, such as accepting or rejecting a good signature by
a key that is not fully trusted, is left to the hook and kept
outside of the core.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Junio C Hamano 2014-08-14 15:59:21 -07:00
parent a85b377d04
commit d05b9618ce
3 changed files with 66 additions and 7 deletions

View File

@ -56,7 +56,21 @@ sha1-old and sha1-new should be valid objects in the repository.
When accepting a signed push (see linkgit:git-push[1]), the signed
push certificate is stored in a blob and an environment variable
`GIT_PUSH_CERT` can be consulted for its object name. See the
description of `post-receive` hook for an example.
description of `post-receive` hook for an example. In addition, the
certificate is verified using GPG and the result is exported with
the following environment variables:
`GIT_PUSH_CERT_SIGNER`::
The name and the e-mail address of the owner of the key that
signed the push certificate.
`GIT_PUSH_CERT_KEY`::
The GPG key ID of the key that signed the push certificate.
`GIT_PUSH_CERT_STATUS`::
The status of GPG verification of the push certificate,
using the same mnemonic as used in `%G?` format of `git log`
family of commands (see linkgit:git-log[1]).
This hook is called before any refname is updated and before any
fast-forward checks are performed.
@ -106,13 +120,13 @@ the update. Refs that were created will have sha1-old equal to
0\{40}, otherwise sha1-old and sha1-new should be valid objects in
the repository.
The `GIT_PUSH_CERT` environment variable can be inspected, just as
The `GIT_PUSH_CERT*` environment variables can be inspected, just as
in `pre-receive` hook, after accepting a signed push.
Using this hook, it is easy to generate mails describing the updates
to the repository. This example script sends one mail message per
ref listing the commits pushed to the repository, and logs the push
certificates of signed pushes to a logger
certificates of signed pushes with good signatures to a logger
service:
#!/bin/sh
@ -130,11 +144,11 @@ service:
mail -s "Changes to ref $ref" commit-list@mydomain
done
# log signed push certificate, if any
if test -n "${GIT_PUSH_CERT-}"
if test -n "${GIT_PUSH_CERT-}" && test ${GIT_PUSH_CERT_STATUS} = G
then
(
git cat-file blob ${GIT_PUSH_CERT}
) | mail -s "push certificate" push-log@mydomain
) | mail -s "push certificate from $GIT_PUSH_CERT_SIGNER" push-log@mydomain
fi
exit 0

View File

@ -15,6 +15,8 @@
#include "connected.h"
#include "argv-array.h"
#include "version.h"
#include "tag.h"
#include "gpg-interface.h"
static const char receive_pack_usage[] = "git receive-pack <git-dir>";
@ -49,6 +51,7 @@ static const char *alt_shallow_file;
static int accept_push_cert = 1;
static struct strbuf push_cert = STRBUF_INIT;
static unsigned char push_cert_sha1[20];
static struct signature_check sigcheck;
static enum deny_action parse_deny_action(const char *var, const char *value)
{
@ -277,12 +280,40 @@ static void prepare_push_cert_sha1(struct child_process *proc)
return;
if (!already_done) {
struct strbuf gpg_output = STRBUF_INIT;
struct strbuf gpg_status = STRBUF_INIT;
int bogs /* beginning_of_gpg_sig */;
already_done = 1;
if (write_sha1_file(push_cert.buf, push_cert.len, "blob", push_cert_sha1))
hashclr(push_cert_sha1);
memset(&sigcheck, '\0', sizeof(sigcheck));
sigcheck.result = 'N';
bogs = parse_signature(push_cert.buf, push_cert.len);
if (verify_signed_buffer(push_cert.buf, bogs,
push_cert.buf + bogs, push_cert.len - bogs,
&gpg_output, &gpg_status) < 0) {
; /* error running gpg */
} else {
sigcheck.payload = push_cert.buf;
sigcheck.gpg_output = gpg_output.buf;
sigcheck.gpg_status = gpg_status.buf;
parse_gpg_output(&sigcheck);
}
strbuf_release(&gpg_output);
strbuf_release(&gpg_status);
}
if (!is_null_sha1(push_cert_sha1)) {
argv_array_pushf(&env, "GIT_PUSH_CERT=%s", sha1_to_hex(push_cert_sha1));
argv_array_pushf(&env, "GIT_PUSH_CERT_SIGNER=%s",
sigcheck.signer ? sigcheck.signer : "");
argv_array_pushf(&env, "GIT_PUSH_CERT_KEY=%s",
sigcheck.key ? sigcheck.key : "");
argv_array_pushf(&env, "GIT_PUSH_CERT_STATUS=%c", sigcheck.result);
proc->env = env.argv;
}
}

View File

@ -83,12 +83,26 @@ test_expect_success GPG 'signed push sends push certificate' '
if test -n "${GIT_PUSH_CERT-}"
then
git cat-file blob $GIT_PUSH_CERT >../push-cert
fi
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}
E_O_F
EOF
cat >expect <<-\EOF &&
SIGNER=C O Mitter <committer@example.com>
KEY=13B6F51ECDDE430D
STATUS=G
EOF
git push --signed dst noop ff +noff &&
grep "$(git rev-parse noop ff) refs/heads/ff" dst/push-cert &&
grep "$(git rev-parse noop noff) refs/heads/noff" dst/push-cert
grep "$(git rev-parse noop noff) refs/heads/noff" dst/push-cert &&
test_cmp expect dst/push-cert-status
'
test_done