Merge branch 'jc/push-cert'
Allow "git push" request to be signed, so that it can be verified and audited, using the GPG signature of the person who pushed, that the tips of branches at a public repository really point the commits the pusher wanted to, without having to "trust" the server. * jc/push-cert: (24 commits) receive-pack::hmac_sha1(): copy the entire SHA-1 hash out signed push: allow stale nonce in stateless mode signed push: teach smart-HTTP to pass "git push --signed" around signed push: fortify against replay attacks signed push: add "pushee" header to push certificate signed push: remove duplicated protocol info send-pack: send feature request on push-cert packet receive-pack: GPG-validate push certificates push: the beginning of "git push --signed" pack-protocol doc: typofix for PKT-LINE gpg-interface: move parse_signature() to where it should be gpg-interface: move parse_gpg_output() to where it should be send-pack: clarify that cmds_sent is a boolean send-pack: refactor inspecting and resetting status and sending commands send-pack: rename "new_refs" to "need_pack_data" receive-pack: factor out capability string generation send-pack: factor out capability string generation send-pack: always send capabilities send-pack: refactor decision to send update per ref send-pack: move REF_STATUS_REJECT_NODELETE logic a bit higher ...
This commit is contained in:
@ -15,6 +15,8 @@
|
||||
#include "connected.h"
|
||||
#include "argv-array.h"
|
||||
#include "version.h"
|
||||
#include "tag.h"
|
||||
#include "gpg-interface.h"
|
||||
#include "sigchain.h"
|
||||
|
||||
static const char receive_pack_usage[] = "git receive-pack <git-dir>";
|
||||
@ -42,11 +44,27 @@ static int prefer_ofs_delta = 1;
|
||||
static int auto_update_server_info;
|
||||
static int auto_gc = 1;
|
||||
static int fix_thin = 1;
|
||||
static int stateless_rpc;
|
||||
static const char *service_dir;
|
||||
static const char *head_name;
|
||||
static void *head_name_to_free;
|
||||
static int sent_capabilities;
|
||||
static int shallow_update;
|
||||
static const char *alt_shallow_file;
|
||||
static struct strbuf push_cert = STRBUF_INIT;
|
||||
static unsigned char push_cert_sha1[20];
|
||||
static struct signature_check sigcheck;
|
||||
static const char *push_cert_nonce;
|
||||
static const char *cert_nonce_seed;
|
||||
|
||||
static const char *NONCE_UNSOLICITED = "UNSOLICITED";
|
||||
static const char *NONCE_BAD = "BAD";
|
||||
static const char *NONCE_MISSING = "MISSING";
|
||||
static const char *NONCE_OK = "OK";
|
||||
static const char *NONCE_SLOP = "SLOP";
|
||||
static const char *nonce_status;
|
||||
static long nonce_stamp_slop;
|
||||
static unsigned long nonce_stamp_slop_limit;
|
||||
|
||||
static enum deny_action parse_deny_action(const char *var, const char *value)
|
||||
{
|
||||
@ -130,6 +148,14 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(var, "receive.certnonceseed") == 0)
|
||||
return git_config_string(&cert_nonce_seed, var, value);
|
||||
|
||||
if (strcmp(var, "receive.certnonceslop") == 0) {
|
||||
nonce_stamp_slop_limit = git_config_ulong(var, value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return git_default_config(var, value, cb);
|
||||
}
|
||||
|
||||
@ -138,15 +164,23 @@ static void show_ref(const char *path, const unsigned char *sha1)
|
||||
if (ref_is_hidden(path))
|
||||
return;
|
||||
|
||||
if (sent_capabilities)
|
||||
if (sent_capabilities) {
|
||||
packet_write(1, "%s %s\n", sha1_to_hex(sha1), path);
|
||||
else
|
||||
packet_write(1, "%s %s%c%s%s agent=%s\n",
|
||||
sha1_to_hex(sha1), path, 0,
|
||||
" report-status delete-refs side-band-64k quiet",
|
||||
prefer_ofs_delta ? " ofs-delta" : "",
|
||||
git_user_agent_sanitized());
|
||||
sent_capabilities = 1;
|
||||
} else {
|
||||
struct strbuf cap = STRBUF_INIT;
|
||||
|
||||
strbuf_addstr(&cap,
|
||||
"report-status delete-refs side-band-64k quiet");
|
||||
if (prefer_ofs_delta)
|
||||
strbuf_addstr(&cap, " ofs-delta");
|
||||
if (push_cert_nonce)
|
||||
strbuf_addf(&cap, " push-cert=%s", push_cert_nonce);
|
||||
strbuf_addf(&cap, " agent=%s", git_user_agent_sanitized());
|
||||
packet_write(1, "%s %s%c%s\n",
|
||||
sha1_to_hex(sha1), path, 0, cap.buf);
|
||||
strbuf_release(&cap);
|
||||
sent_capabilities = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int show_ref_cb(const char *path, const unsigned char *sha1, int flag, void *unused)
|
||||
@ -253,6 +287,222 @@ static int copy_to_sideband(int in, int out, void *arg)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define HMAC_BLOCK_SIZE 64
|
||||
|
||||
static void hmac_sha1(unsigned char *out,
|
||||
const char *key_in, size_t key_len,
|
||||
const char *text, size_t text_len)
|
||||
{
|
||||
unsigned char key[HMAC_BLOCK_SIZE];
|
||||
unsigned char k_ipad[HMAC_BLOCK_SIZE];
|
||||
unsigned char k_opad[HMAC_BLOCK_SIZE];
|
||||
int i;
|
||||
git_SHA_CTX ctx;
|
||||
|
||||
/* RFC 2104 2. (1) */
|
||||
memset(key, '\0', HMAC_BLOCK_SIZE);
|
||||
if (HMAC_BLOCK_SIZE < key_len) {
|
||||
git_SHA1_Init(&ctx);
|
||||
git_SHA1_Update(&ctx, key_in, key_len);
|
||||
git_SHA1_Final(key, &ctx);
|
||||
} else {
|
||||
memcpy(key, key_in, key_len);
|
||||
}
|
||||
|
||||
/* RFC 2104 2. (2) & (5) */
|
||||
for (i = 0; i < sizeof(key); i++) {
|
||||
k_ipad[i] = key[i] ^ 0x36;
|
||||
k_opad[i] = key[i] ^ 0x5c;
|
||||
}
|
||||
|
||||
/* RFC 2104 2. (3) & (4) */
|
||||
git_SHA1_Init(&ctx);
|
||||
git_SHA1_Update(&ctx, k_ipad, sizeof(k_ipad));
|
||||
git_SHA1_Update(&ctx, text, text_len);
|
||||
git_SHA1_Final(out, &ctx);
|
||||
|
||||
/* RFC 2104 2. (6) & (7) */
|
||||
git_SHA1_Init(&ctx);
|
||||
git_SHA1_Update(&ctx, k_opad, sizeof(k_opad));
|
||||
git_SHA1_Update(&ctx, out, 20);
|
||||
git_SHA1_Final(out, &ctx);
|
||||
}
|
||||
|
||||
static char *prepare_push_cert_nonce(const char *path, unsigned long stamp)
|
||||
{
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
unsigned char sha1[20];
|
||||
|
||||
strbuf_addf(&buf, "%s:%lu", path, stamp);
|
||||
hmac_sha1(sha1, buf.buf, buf.len, cert_nonce_seed, strlen(cert_nonce_seed));;
|
||||
strbuf_release(&buf);
|
||||
|
||||
/* RFC 2104 5. HMAC-SHA1-80 */
|
||||
strbuf_addf(&buf, "%lu-%.*s", stamp, 20, sha1_to_hex(sha1));
|
||||
return strbuf_detach(&buf, NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
* NEEDSWORK: reuse find_commit_header() from jk/commit-author-parsing
|
||||
* after dropping "_commit" from its name and possibly moving it out
|
||||
* of commit.c
|
||||
*/
|
||||
static char *find_header(const char *msg, size_t len, const char *key)
|
||||
{
|
||||
int key_len = strlen(key);
|
||||
const char *line = msg;
|
||||
|
||||
while (line && line < msg + len) {
|
||||
const char *eol = strchrnul(line, '\n');
|
||||
|
||||
if ((msg + len <= eol) || line == eol)
|
||||
return NULL;
|
||||
if (line + key_len < eol &&
|
||||
!memcmp(line, key, key_len) && line[key_len] == ' ') {
|
||||
int offset = key_len + 1;
|
||||
return xmemdupz(line + offset, (eol - line) - offset);
|
||||
}
|
||||
line = *eol ? eol + 1 : NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *check_nonce(const char *buf, size_t len)
|
||||
{
|
||||
char *nonce = find_header(buf, len, "nonce");
|
||||
unsigned long stamp, ostamp;
|
||||
char *bohmac, *expect = NULL;
|
||||
const char *retval = NONCE_BAD;
|
||||
|
||||
if (!nonce) {
|
||||
retval = NONCE_MISSING;
|
||||
goto leave;
|
||||
} else if (!push_cert_nonce) {
|
||||
retval = NONCE_UNSOLICITED;
|
||||
goto leave;
|
||||
} else if (!strcmp(push_cert_nonce, nonce)) {
|
||||
retval = NONCE_OK;
|
||||
goto leave;
|
||||
}
|
||||
|
||||
if (!stateless_rpc) {
|
||||
/* returned nonce MUST match what we gave out earlier */
|
||||
retval = NONCE_BAD;
|
||||
goto leave;
|
||||
}
|
||||
|
||||
/*
|
||||
* In stateless mode, we may be receiving a nonce issued by
|
||||
* another instance of the server that serving the same
|
||||
* repository, and the timestamps may not match, but the
|
||||
* nonce-seed and dir should match, so we can recompute and
|
||||
* report the time slop.
|
||||
*
|
||||
* In addition, when a nonce issued by another instance has
|
||||
* timestamp within receive.certnonceslop seconds, we pretend
|
||||
* as if we issued that nonce when reporting to the hook.
|
||||
*/
|
||||
|
||||
/* nonce is concat(<seconds-since-epoch>, "-", <hmac>) */
|
||||
if (*nonce <= '0' || '9' < *nonce) {
|
||||
retval = NONCE_BAD;
|
||||
goto leave;
|
||||
}
|
||||
stamp = strtoul(nonce, &bohmac, 10);
|
||||
if (bohmac == nonce || bohmac[0] != '-') {
|
||||
retval = NONCE_BAD;
|
||||
goto leave;
|
||||
}
|
||||
|
||||
expect = prepare_push_cert_nonce(service_dir, stamp);
|
||||
if (strcmp(expect, nonce)) {
|
||||
/* Not what we would have signed earlier */
|
||||
retval = NONCE_BAD;
|
||||
goto leave;
|
||||
}
|
||||
|
||||
/*
|
||||
* By how many seconds is this nonce stale? Negative value
|
||||
* would mean it was issued by another server with its clock
|
||||
* skewed in the future.
|
||||
*/
|
||||
ostamp = strtoul(push_cert_nonce, NULL, 10);
|
||||
nonce_stamp_slop = (long)ostamp - (long)stamp;
|
||||
|
||||
if (nonce_stamp_slop_limit &&
|
||||
abs(nonce_stamp_slop) <= nonce_stamp_slop_limit) {
|
||||
/*
|
||||
* Pretend as if the received nonce (which passes the
|
||||
* HMAC check, so it is not a forged by third-party)
|
||||
* is what we issued.
|
||||
*/
|
||||
free((void *)push_cert_nonce);
|
||||
push_cert_nonce = xstrdup(nonce);
|
||||
retval = NONCE_OK;
|
||||
} else {
|
||||
retval = NONCE_SLOP;
|
||||
}
|
||||
|
||||
leave:
|
||||
free(nonce);
|
||||
free(expect);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static void prepare_push_cert_sha1(struct child_process *proc)
|
||||
{
|
||||
static int already_done;
|
||||
struct argv_array env = ARGV_ARRAY_INIT;
|
||||
|
||||
if (!push_cert.len)
|
||||
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);
|
||||
nonce_status = check_nonce(push_cert.buf, bogs);
|
||||
}
|
||||
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);
|
||||
if (push_cert_nonce) {
|
||||
argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE=%s", push_cert_nonce);
|
||||
argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_STATUS=%s", nonce_status);
|
||||
if (nonce_status == NONCE_SLOP)
|
||||
argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_SLOP=%ld",
|
||||
nonce_stamp_slop);
|
||||
}
|
||||
proc->env = env.argv;
|
||||
}
|
||||
}
|
||||
|
||||
typedef int (*feed_fn)(void *, const char **, size_t *);
|
||||
static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_state)
|
||||
{
|
||||
@ -271,6 +521,8 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta
|
||||
proc.in = -1;
|
||||
proc.stdout_to_stderr = 1;
|
||||
|
||||
prepare_push_cert_sha1(&proc);
|
||||
|
||||
if (use_sideband) {
|
||||
memset(&muxer, 0, sizeof(muxer));
|
||||
muxer.proc = copy_to_sideband;
|
||||
@ -841,40 +1093,79 @@ static void execute_commands(struct command *commands,
|
||||
"the reported refs above");
|
||||
}
|
||||
|
||||
static struct command **queue_command(struct command **tail,
|
||||
const char *line,
|
||||
int linelen)
|
||||
{
|
||||
unsigned char old_sha1[20], new_sha1[20];
|
||||
struct command *cmd;
|
||||
const char *refname;
|
||||
int reflen;
|
||||
|
||||
if (linelen < 83 ||
|
||||
line[40] != ' ' ||
|
||||
line[81] != ' ' ||
|
||||
get_sha1_hex(line, old_sha1) ||
|
||||
get_sha1_hex(line + 41, new_sha1))
|
||||
die("protocol error: expected old/new/ref, got '%s'", line);
|
||||
|
||||
refname = line + 82;
|
||||
reflen = linelen - 82;
|
||||
cmd = xcalloc(1, sizeof(struct command) + reflen + 1);
|
||||
hashcpy(cmd->old_sha1, old_sha1);
|
||||
hashcpy(cmd->new_sha1, new_sha1);
|
||||
memcpy(cmd->ref_name, refname, reflen);
|
||||
cmd->ref_name[reflen] = '\0';
|
||||
*tail = cmd;
|
||||
return &cmd->next;
|
||||
}
|
||||
|
||||
static void queue_commands_from_cert(struct command **tail,
|
||||
struct strbuf *push_cert)
|
||||
{
|
||||
const char *boc, *eoc;
|
||||
|
||||
if (*tail)
|
||||
die("protocol error: got both push certificate and unsigned commands");
|
||||
|
||||
boc = strstr(push_cert->buf, "\n\n");
|
||||
if (!boc)
|
||||
die("malformed push certificate %.*s", 100, push_cert->buf);
|
||||
else
|
||||
boc += 2;
|
||||
eoc = push_cert->buf + parse_signature(push_cert->buf, push_cert->len);
|
||||
|
||||
while (boc < eoc) {
|
||||
const char *eol = memchr(boc, '\n', eoc - boc);
|
||||
tail = queue_command(tail, boc, eol ? eol - boc : eoc - eol);
|
||||
boc = eol ? eol + 1 : eoc;
|
||||
}
|
||||
}
|
||||
|
||||
static struct command *read_head_info(struct sha1_array *shallow)
|
||||
{
|
||||
struct command *commands = NULL;
|
||||
struct command **p = &commands;
|
||||
for (;;) {
|
||||
char *line;
|
||||
unsigned char old_sha1[20], new_sha1[20];
|
||||
struct command *cmd;
|
||||
char *refname;
|
||||
int len, reflen;
|
||||
int len, linelen;
|
||||
|
||||
line = packet_read_line(0, &len);
|
||||
if (!line)
|
||||
break;
|
||||
|
||||
if (len == 48 && starts_with(line, "shallow ")) {
|
||||
if (get_sha1_hex(line + 8, old_sha1))
|
||||
die("protocol error: expected shallow sha, got '%s'", line + 8);
|
||||
sha1_array_append(shallow, old_sha1);
|
||||
unsigned char sha1[20];
|
||||
if (get_sha1_hex(line + 8, sha1))
|
||||
die("protocol error: expected shallow sha, got '%s'",
|
||||
line + 8);
|
||||
sha1_array_append(shallow, sha1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (len < 83 ||
|
||||
line[40] != ' ' ||
|
||||
line[81] != ' ' ||
|
||||
get_sha1_hex(line, old_sha1) ||
|
||||
get_sha1_hex(line + 41, new_sha1))
|
||||
die("protocol error: expected old/new/ref, got '%s'",
|
||||
line);
|
||||
|
||||
refname = line + 82;
|
||||
reflen = strlen(refname);
|
||||
if (reflen + 82 < len) {
|
||||
const char *feature_list = refname + reflen + 1;
|
||||
linelen = strlen(line);
|
||||
if (linelen < len) {
|
||||
const char *feature_list = line + linelen + 1;
|
||||
if (parse_feature_request(feature_list, "report-status"))
|
||||
report_status = 1;
|
||||
if (parse_feature_request(feature_list, "side-band-64k"))
|
||||
@ -882,13 +1173,34 @@ static struct command *read_head_info(struct sha1_array *shallow)
|
||||
if (parse_feature_request(feature_list, "quiet"))
|
||||
quiet = 1;
|
||||
}
|
||||
cmd = xcalloc(1, sizeof(struct command) + len - 80);
|
||||
hashcpy(cmd->old_sha1, old_sha1);
|
||||
hashcpy(cmd->new_sha1, new_sha1);
|
||||
memcpy(cmd->ref_name, line + 82, len - 81);
|
||||
*p = cmd;
|
||||
p = &cmd->next;
|
||||
|
||||
if (!strcmp(line, "push-cert")) {
|
||||
int true_flush = 0;
|
||||
char certbuf[1024];
|
||||
|
||||
for (;;) {
|
||||
len = packet_read(0, NULL, NULL,
|
||||
certbuf, sizeof(certbuf), 0);
|
||||
if (!len) {
|
||||
true_flush = 1;
|
||||
break;
|
||||
}
|
||||
if (!strcmp(certbuf, "push-cert-end\n"))
|
||||
break; /* end of cert */
|
||||
strbuf_addstr(&push_cert, certbuf);
|
||||
}
|
||||
|
||||
if (true_flush)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
p = queue_command(p, line, linelen);
|
||||
}
|
||||
|
||||
if (push_cert.len)
|
||||
queue_commands_from_cert(p, &push_cert);
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
@ -1129,9 +1441,7 @@ static int delete_only(struct command *commands)
|
||||
int cmd_receive_pack(int argc, const char **argv, const char *prefix)
|
||||
{
|
||||
int advertise_refs = 0;
|
||||
int stateless_rpc = 0;
|
||||
int i;
|
||||
const char *dir = NULL;
|
||||
struct command *commands;
|
||||
struct sha1_array shallow = SHA1_ARRAY_INIT;
|
||||
struct sha1_array ref = SHA1_ARRAY_INIT;
|
||||
@ -1164,19 +1474,21 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
|
||||
|
||||
usage(receive_pack_usage);
|
||||
}
|
||||
if (dir)
|
||||
if (service_dir)
|
||||
usage(receive_pack_usage);
|
||||
dir = arg;
|
||||
service_dir = arg;
|
||||
}
|
||||
if (!dir)
|
||||
if (!service_dir)
|
||||
usage(receive_pack_usage);
|
||||
|
||||
setup_path();
|
||||
|
||||
if (!enter_repo(dir, 0))
|
||||
die("'%s' does not appear to be a git repository", dir);
|
||||
if (!enter_repo(service_dir, 0))
|
||||
die("'%s' does not appear to be a git repository", service_dir);
|
||||
|
||||
git_config(receive_pack_config, NULL);
|
||||
if (cert_nonce_seed)
|
||||
push_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL));
|
||||
|
||||
if (0 <= transfer_unpack_limit)
|
||||
unpack_limit = transfer_unpack_limit;
|
||||
@ -1221,5 +1533,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
|
||||
packet_flush(1);
|
||||
sha1_array_clear(&shallow);
|
||||
sha1_array_clear(&ref);
|
||||
free((void *)push_cert_nonce);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user