From 3bfcb95fa84d8bacb01a990c5bdb16df13462279 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 15 Aug 2014 13:53:46 -0700 Subject: receive-pack: do not overallocate command structure An "update" command in the protocol exchange consists of 40-hex old object name, SP, 40-hex new object name, SP, and a refname, but the first instance is further followed by a NUL with feature requests. The command structure, which has a flex-array member that stores the refname at the end, was allocated based on the whole length of the update command, without excluding the trailing feature requests. Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index f93ac454b4..1663bebaa2 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -872,10 +872,11 @@ 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); + cmd = xcalloc(1, sizeof(struct command) + reflen + 1); hashcpy(cmd->old_sha1, old_sha1); hashcpy(cmd->new_sha1, new_sha1); - memcpy(cmd->ref_name, line + 82, len - 81); + memcpy(cmd->ref_name, refname, reflen); + cmd->ref_name[reflen] = '\0'; *p = cmd; p = &cmd->next; } -- cgit v1.2.3 From 0e3c339bb69670b39161838de17f440480e1358a Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 15 Aug 2014 14:11:33 -0700 Subject: receive-pack: parse feature request a bit earlier Ideally, we should have also allowed the first "shallow" to carry the feature request trailer, but that is water under the bridge now. This makes the next step to factor out the queuing of commands easier to review. Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 1663bebaa2..a91eec8b1c 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -840,7 +840,7 @@ static struct command *read_head_info(struct sha1_array *shallow) unsigned char old_sha1[20], new_sha1[20]; struct command *cmd; char *refname; - int len, reflen; + int len, reflen, linelen; line = packet_read_line(0, &len); if (!line) @@ -853,7 +853,18 @@ static struct command *read_head_info(struct sha1_array *shallow) continue; } - if (len < 83 || + 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")) + use_sideband = LARGE_PACKET_MAX; + if (parse_feature_request(feature_list, "quiet")) + quiet = 1; + } + + if (linelen < 83 || line[40] != ' ' || line[81] != ' ' || get_sha1_hex(line, old_sha1) || @@ -862,16 +873,7 @@ static struct command *read_head_info(struct sha1_array *shallow) line); refname = line + 82; - reflen = strlen(refname); - if (reflen + 82 < len) { - const char *feature_list = refname + reflen + 1; - if (parse_feature_request(feature_list, "report-status")) - report_status = 1; - if (parse_feature_request(feature_list, "side-band-64k")) - use_sideband = LARGE_PACKET_MAX; - if (parse_feature_request(feature_list, "quiet")) - quiet = 1; - } + reflen = linelen - 82; cmd = xcalloc(1, sizeof(struct command) + reflen + 1); hashcpy(cmd->old_sha1, old_sha1); hashcpy(cmd->new_sha1, new_sha1); -- cgit v1.2.3 From c09b71ccc45f4be3acca5e809a18a000cae09faf Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 15 Aug 2014 14:26:17 -0700 Subject: receive-pack: do not reuse old_sha1[] for other things This piece of code reads object names of shallow boundaries, not old_sha1[], i.e. the current value the ref points at, which is to be replaced by what is in new_sha1[]. Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index a91eec8b1c..c9b92bffda 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -847,9 +847,11 @@ static struct command *read_head_info(struct sha1_array *shallow) 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; } -- cgit v1.2.3 From 39895c74d809962bf76a6d720618df30f4bac8b1 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 15 Aug 2014 14:28:28 -0700 Subject: receive-pack: factor out queueing of command Make a helper function to accept a line of a protocol message and queue an update command out of the code from read_head_info(). Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index c9b92bffda..587f7da559 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -831,16 +831,40 @@ 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 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, linelen; + int len, linelen; line = packet_read_line(0, &len); if (!line) @@ -866,23 +890,7 @@ static struct command *read_head_info(struct sha1_array *shallow) quiet = 1; } - 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'; - *p = cmd; - p = &cmd->next; + p = queue_command(p, line, linelen); } return commands; } -- cgit v1.2.3 From 52d2ae582e84c6d4ace8ab7e021808915148816a Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 4 Sep 2014 12:13:32 -0700 Subject: receive-pack: factor out capability string generation Similar to the previous one for send-pack, make it easier and cleaner to add to capability advertisement. Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 587f7da559..cbbad5488a 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -137,15 +137,21 @@ 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"); + 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) -- cgit v1.2.3 From a85b377d0419a9dfaca8af2320cc33b051cbed04 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 12 Sep 2014 11:17:07 -0700 Subject: push: the beginning of "git push --signed" While signed tags and commits assert that the objects thusly signed came from you, who signed these objects, there is not a good way to assert that you wanted to have a particular object at the tip of a particular branch. My signing v2.0.1 tag only means I want to call the version v2.0.1, and it does not mean I want to push it out to my 'master' branch---it is likely that I only want it in 'maint', so the signature on the object alone is insufficient. The only assurance to you that 'maint' points at what I wanted to place there comes from your trust on the hosting site and my authentication with it, which cannot easily audited later. Introduce a mechanism that allows you to sign a "push certificate" (for the lack of better name) every time you push, asserting that what object you are pushing to update which ref that used to point at what other object. Think of it as a cryptographic protection for ref updates, similar to signed tags/commits but working on an orthogonal axis. The basic flow based on this mechanism goes like this: 1. You push out your work with "git push --signed". 2. The sending side learns where the remote refs are as usual, together with what protocol extension the receiving end supports. If the receiving end does not advertise the protocol extension "push-cert", an attempt to "git push --signed" fails. Otherwise, a text file, that looks like the following, is prepared in core: certificate version 0.1 pusher Junio C Hamano 1315427886 -0700 7339ca65... 21580ecb... refs/heads/master 3793ac56... 12850bec... refs/heads/next The file begins with a few header lines, which may grow as we gain more experience. The 'pusher' header records the name of the signer (the value of user.signingkey configuration variable, falling back to GIT_COMMITTER_{NAME|EMAIL}) and the time of the certificate generation. After the header, a blank line follows, followed by a copy of the protocol message lines. Each line shows the old and the new object name at the tip of the ref this push tries to update, in the way identical to how the underlying "git push" protocol exchange tells the ref updates to the receiving end (by recording the "old" object name, the push certificate also protects against replaying). It is expected that new command packet types other than the old-new-refname kind will be included in push certificate in the same way as would appear in the plain vanilla command packets in unsigned pushes. The user then is asked to sign this push certificate using GPG, formatted in a way similar to how signed tag objects are signed, and the result is sent to the other side (i.e. receive-pack). In the protocol exchange, this step comes immediately before the sender tells what the result of the push should be, which in turn comes before it sends the pack data. 3. When the receiving end sees a push certificate, the certificate is written out as a blob. The pre-receive hook can learn about the certificate by checking GIT_PUSH_CERT environment variable, which, if present, tells the object name of this blob, and make the decision to allow or reject this push. Additionally, the post-receive hook can also look at the certificate, which may be a good place to log all the received certificates for later audits. Because a push certificate carry the same information as the usual command packets in the protocol exchange, we can omit the latter when a push certificate is in use and reduce the protocol overhead. This however is not included in this patch to make it easier to review (in other words, the series at this step should never be released without the remainder of the series, as it implements an interim protocol that will be incompatible with the final one). As such, the documentation update for the protocol is left out of this step. Signed-off-by: Junio C Hamano --- Documentation/config.txt | 6 +++ Documentation/git-push.txt | 9 +++- Documentation/git-receive-pack.txt | 19 +++++++- builtin/push.c | 1 + builtin/receive-pack.c | 52 +++++++++++++++++++++ send-pack.c | 64 ++++++++++++++++++++++++++ send-pack.h | 1 + t/t5534-push-signed.sh | 94 ++++++++++++++++++++++++++++++++++++++ transport.c | 4 ++ transport.h | 5 ++ 10 files changed, 253 insertions(+), 2 deletions(-) create mode 100755 t/t5534-push-signed.sh (limited to 'builtin/receive-pack.c') diff --git a/Documentation/config.txt b/Documentation/config.txt index c55c22ab7b..0d01e32888 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2038,6 +2038,12 @@ rebase.autostash:: successful rebase might result in non-trivial conflicts. Defaults to false. +receive.acceptpushcert:: + By default, `git receive-pack` will advertise that it + accepts `git push --signed`. Setting this variable to + false disables it (this is a tentative variable that + will go away at the end of this series). + receive.autogc:: By default, git-receive-pack will run "git-gc --auto" after receiving data from git-push and updating refs. You can stop diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt index 21cd455508..21b3f29c3b 100644 --- a/Documentation/git-push.txt +++ b/Documentation/git-push.txt @@ -10,7 +10,8 @@ SYNOPSIS -------- [verse] 'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=] - [--repo=] [-f | --force] [--prune] [-v | --verbose] [-u | --set-upstream] + [--repo=] [-f | --force] [--prune] [-v | --verbose] + [-u | --set-upstream] [--signed] [--force-with-lease[=[:]]] [--no-verify] [ [...]] @@ -129,6 +130,12 @@ already exists on the remote side. from the remote but are pointing at commit-ish that are reachable from the refs being pushed. +--signed:: + GPG-sign the push request to update refs on the receiving + side, to allow it to be checked by the hooks and/or be + logged. See linkgit:git-receive-pack[1] for the details + on the receiving end. + --receive-pack=:: --exec=:: Path to the 'git-receive-pack' program on the remote diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt index b1f7dc643a..a2dd74376c 100644 --- a/Documentation/git-receive-pack.txt +++ b/Documentation/git-receive-pack.txt @@ -53,6 +53,11 @@ the update. Refs to be created will have sha1-old equal to 0\{40}, while refs to be deleted will have sha1-new equal to 0\{40}, otherwise 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. + This hook is called before any refname is updated and before any fast-forward checks are performed. @@ -101,9 +106,14 @@ 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 +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: +ref listing the commits pushed to the repository, and logs the push +certificates of signed pushes to a logger +service: #!/bin/sh # mail out commit update information. @@ -119,6 +129,13 @@ ref listing the commits pushed to the repository: fi | mail -s "Changes to ref $ref" commit-list@mydomain done + # log signed push certificate, if any + if test -n "${GIT_PUSH_CERT-}" + then + ( + git cat-file blob ${GIT_PUSH_CERT} + ) | mail -s "push certificate" push-log@mydomain + fi exit 0 The exit code from this hook invocation is ignored, however a diff --git a/builtin/push.c b/builtin/push.c index f50e3d5e77..ae56f73a66 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -506,6 +506,7 @@ int cmd_push(int argc, const char **argv, const char *prefix) OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK), OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"), TRANSPORT_PUSH_FOLLOW_TAGS), + OPT_BIT(0, "signed", &flags, N_("GPG sign the push"), TRANSPORT_PUSH_CERT), OPT_END() }; diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index cbbad5488a..610b085e3d 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -46,6 +46,9 @@ static void *head_name_to_free; static int sent_capabilities; static int shallow_update; 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 enum deny_action parse_deny_action(const char *var, const char *value) { @@ -129,6 +132,11 @@ static int receive_pack_config(const char *var, const char *value, void *cb) return 0; } + if (strcmp(var, "receive.acceptpushcert") == 0) { + accept_push_cert = git_config_bool(var, value); + return 0; + } + return git_default_config(var, value, cb); } @@ -146,6 +154,8 @@ static void show_ref(const char *path, const unsigned char *sha1) "report-status delete-refs side-band-64k quiet"); if (prefer_ofs_delta) strbuf_addstr(&cap, " ofs-delta"); + if (accept_push_cert) + strbuf_addstr(&cap, " push-cert"); 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); @@ -258,6 +268,25 @@ static int copy_to_sideband(int in, int out, void *arg) return 0; } +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) { + already_done = 1; + if (write_sha1_file(push_cert.buf, push_cert.len, "blob", push_cert_sha1)) + hashclr(push_cert_sha1); + } + if (!is_null_sha1(push_cert_sha1)) { + argv_array_pushf(&env, "GIT_PUSH_CERT=%s", sha1_to_hex(push_cert_sha1)); + 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) { @@ -277,6 +306,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; @@ -896,6 +927,27 @@ static struct command *read_head_info(struct sha1_array *shallow) quiet = 1; } + 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); } return commands; diff --git a/send-pack.c b/send-pack.c index bb13599c33..ef93f33aa5 100644 --- a/send-pack.c +++ b/send-pack.c @@ -11,6 +11,7 @@ #include "transport.h" #include "version.h" #include "sha1-array.h" +#include "gpg-interface.h" static int feed_object(const unsigned char *sha1, int fd, int negative) { @@ -210,6 +211,64 @@ static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_a } } +/* + * the beginning of the next line, or the end of buffer. + * + * NEEDSWORK: perhaps move this to git-compat-util.h or somewhere and + * convert many similar uses found by "git grep -A4 memchr". + */ +static const char *next_line(const char *line, size_t len) +{ + const char *nl = memchr(line, '\n', len); + if (!nl) + return line + len; /* incomplete line */ + return nl + 1; +} + +static void generate_push_cert(struct strbuf *req_buf, + const struct ref *remote_refs, + struct send_pack_args *args) +{ + const struct ref *ref; + char stamp[60]; + char *signing_key = xstrdup(get_signing_key()); + const char *cp, *np; + struct strbuf cert = STRBUF_INIT; + int update_seen = 0; + + datestamp(stamp, sizeof(stamp)); + strbuf_addf(&cert, "certificate version 0.1\n"); + strbuf_addf(&cert, "pusher %s %s\n", signing_key, stamp); + strbuf_addstr(&cert, "\n"); + + for (ref = remote_refs; ref; ref = ref->next) { + if (!ref_update_to_be_sent(ref, args)) + continue; + update_seen = 1; + strbuf_addf(&cert, "%s %s %s\n", + sha1_to_hex(ref->old_sha1), + sha1_to_hex(ref->new_sha1), + ref->name); + } + if (!update_seen) + goto free_return; + + if (sign_buffer(&cert, &cert, signing_key)) + die(_("failed to sign the push certificate")); + + packet_buf_write(req_buf, "push-cert\n"); + for (cp = cert.buf; cp < cert.buf + cert.len; cp = np) { + np = next_line(cp, cert.buf + cert.len - cp); + packet_buf_write(req_buf, + "%.*s", (int)(np - cp), cp); + } + packet_buf_write(req_buf, "push-cert-end\n"); + +free_return: + free(signing_key); + strbuf_release(&cert); +} + int send_pack(struct send_pack_args *args, int fd[], struct child_process *conn, struct ref *remote_refs, @@ -245,6 +304,8 @@ int send_pack(struct send_pack_args *args, agent_supported = 1; if (server_supports("no-thin")) args->use_thin_pack = 0; + if (args->push_cert && !server_supports("push-cert")) + die(_("the receiving end does not support --signed push")); if (!remote_refs) { fprintf(stderr, "No refs in common and none specified; doing nothing.\n" @@ -273,6 +334,9 @@ int send_pack(struct send_pack_args *args, if (!args->dry_run) advertise_shallow_grafts_buf(&req_buf); + if (!args->dry_run && args->push_cert) + generate_push_cert(&req_buf, remote_refs, args); + /* * Clear the status for each ref and see if we need to send * the pack data. diff --git a/send-pack.h b/send-pack.h index 8e843924cf..3555d8e8ad 100644 --- a/send-pack.h +++ b/send-pack.h @@ -11,6 +11,7 @@ struct send_pack_args { use_thin_pack:1, use_ofs_delta:1, dry_run:1, + push_cert:1, stateless_rpc:1; }; diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh new file mode 100755 index 0000000000..019ac71506 --- /dev/null +++ b/t/t5534-push-signed.sh @@ -0,0 +1,94 @@ +#!/bin/sh + +test_description='signed push' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-gpg.sh + +prepare_dst () { + rm -fr dst && + test_create_repo dst && + + git push dst master:noop master:ff master:noff +} + +test_expect_success setup ' + # master, ff and noff branches pointing at the same commit + test_tick && + git commit --allow-empty -m initial && + + git checkout -b noop && + git checkout -b ff && + git checkout -b noff && + + # noop stays the same, ff advances, noff rewrites + test_tick && + git commit --allow-empty --amend -m rewritten && + git checkout ff && + + test_tick && + git commit --allow-empty -m second +' + +test_expect_success 'unsigned push does not send push certificate' ' + prepare_dst && + mkdir -p dst/.git/hooks && + 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 + EOF + + git push dst noop ff +noff && + ! test -f dst/push-cert +' + +test_expect_success 'talking with a receiver without push certificate support' ' + prepare_dst && + mkdir -p dst/.git/hooks && + git -C dst config receive.acceptpushcert no && + 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 + EOF + + git push dst noop ff +noff && + ! test -f dst/push-cert +' + +test_expect_success 'push --signed fails with a receiver without push certificate support' ' + prepare_dst && + mkdir -p dst/.git/hooks && + git -C dst config receive.acceptpushcert no && + test_must_fail git push --signed dst noop ff +noff 2>err && + test_i18ngrep "the receiving end does not support" err +' + +test_expect_success GPG 'signed push sends push certificate' ' + prepare_dst && + mkdir -p dst/.git/hooks && + 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 + 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 +' + +test_done diff --git a/transport.c b/transport.c index 662421bb5e..07fdf86494 100644 --- a/transport.c +++ b/transport.c @@ -480,6 +480,9 @@ static int set_git_option(struct git_transport_options *opts, die("transport: invalid depth option '%s'", value); } return 0; + } else if (!strcmp(name, TRANS_OPT_PUSH_CERT)) { + opts->push_cert = !!value; + return 0; } return 1; } @@ -823,6 +826,7 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re args.progress = transport->progress; args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN); args.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN); + args.push_cert = !!(flags & TRANSPORT_PUSH_CERT); ret = send_pack(&args, data->fd, data->conn, remote_refs, &data->extra_have); diff --git a/transport.h b/transport.h index 02ea248db1..3e0091eaab 100644 --- a/transport.h +++ b/transport.h @@ -12,6 +12,7 @@ struct git_transport_options { unsigned check_self_contained_and_connected : 1; unsigned self_contained_and_connected : 1; unsigned update_shallow : 1; + unsigned push_cert : 1; int depth; const char *uploadpack; const char *receivepack; @@ -123,6 +124,7 @@ struct transport { #define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256 #define TRANSPORT_PUSH_NO_HOOK 512 #define TRANSPORT_PUSH_FOLLOW_TAGS 1024 +#define TRANSPORT_PUSH_CERT 2048 #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3) #define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x) @@ -156,6 +158,9 @@ struct transport *transport_get(struct remote *, const char *); /* Accept refs that may update .git/shallow without --depth */ #define TRANS_OPT_UPDATE_SHALLOW "updateshallow" +/* Send push certificates */ +#define TRANS_OPT_PUSH_CERT "pushcert" + /** * Returns 0 if the option was used, non-zero otherwise. Prints a * message to stderr if the option is not used. -- cgit v1.2.3 From d05b9618ce42e85936176537f939a4eb85d4d65e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 14 Aug 2014 15:59:21 -0700 Subject: 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 --- Documentation/git-receive-pack.txt | 24 +++++++++++++++++++----- builtin/receive-pack.c | 31 +++++++++++++++++++++++++++++++ t/t5534-push-signed.sh | 18 ++++++++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt index a2dd74376c..e6df234926 100644 --- a/Documentation/git-receive-pack.txt +++ b/Documentation/git-receive-pack.txt @@ -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 diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 610b085e3d..c0a3189943 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -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 "; @@ -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; } } diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh index 019ac71506..4198b6a2fb 100755 --- a/t/t5534-push-signed.sh +++ b/t/t5534-push-signed.sh @@ -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 <expect <<-\EOF && + SIGNER=C O Mitter + 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 -- cgit v1.2.3 From 4adf569dea052dac88121d822e11c249986b3398 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 18 Aug 2014 14:38:45 -0700 Subject: signed push: remove duplicated protocol info With the interim protocol, we used to send the update commands even though we already send a signed copy of the same information when push certificate is in use. Update the send-pack/receive-pack pair not to do so. The notable thing on the receive-pack side is that it makes sure that there is no command sent over the traditional protocol packet outside the push certificate. Otherwise a pusher can claim to be pushing one set of ref updates in the signed certificate while issuing commands to update unrelated refs, and such an update will evade later audits. Finally, start documenting the protocol. Signed-off-by: Junio C Hamano --- Documentation/technical/pack-protocol.txt | 33 ++++++++++++++++++++++- Documentation/technical/protocol-capabilities.txt | 12 +++++++-- builtin/receive-pack.c | 26 ++++++++++++++++++ send-pack.c | 2 +- 4 files changed, 69 insertions(+), 4 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt index a845d51b1b..4a5c2e8636 100644 --- a/Documentation/technical/pack-protocol.txt +++ b/Documentation/technical/pack-protocol.txt @@ -465,7 +465,7 @@ contain all the objects that the server will need to complete the new references. ---- - update-request = *shallow command-list [pack-file] + update-request = *shallow ( command-list | push-cert ) [pack-file] shallow = PKT-LINE("shallow" SP obj-id) @@ -481,12 +481,25 @@ references. old-id = obj-id new-id = obj-id + push-cert = PKT-LINE("push-cert" NUL capability-list LF) + PKT-LINE("certificate version 0.1" LF) + PKT-LINE("pusher" SP ident LF) + PKT-LINE(LF) + *PKT-LINE(command LF) + *PKT-LINE(gpg-signature-lines LF) + PKT-LINE("push-cert-end" LF) + pack-file = "PACK" 28*(OCTET) ---- If the receiving end does not support delete-refs, the sending end MUST NOT ask for delete command. +If the receiving end does not support push-cert, the sending end +MUST NOT send a push-cert command. When a push-cert command is +sent, command-list MUST NOT be sent; the commands recorded in the +push certificate is used instead. + The pack-file MUST NOT be sent if the only command used is 'delete'. A pack-file MUST be sent if either create or update command is used, @@ -501,6 +514,24 @@ was being processed (the obj-id is still the same as the old-id), and it will run any update hooks to make sure that the update is acceptable. If all of that is fine, the server will then update the references. +Push Certificate +---------------- + +A push certificate begins with a set of header lines. After the +header and an empty line, the protocol commands follow, one per +line. + +Currently, the following header fields are defined: + +`pusher` ident:: + Identify the GPG key in "Human Readable Name " + format. + +The GPG signature lines are a detached signature for the contents +recorded in the push certificate before the signature block begins. +The detached signature is used to certify that the commands were +given by the pusher, who must be the signer. + Report Status ------------- diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt index e174343847..a478cc4135 100644 --- a/Documentation/technical/protocol-capabilities.txt +++ b/Documentation/technical/protocol-capabilities.txt @@ -18,8 +18,8 @@ was sent. Server MUST NOT ignore capabilities that client requested and server advertised. As a consequence of these rules, server MUST NOT advertise capabilities it does not understand. -The 'report-status', 'delete-refs', and 'quiet' capabilities are sent and -recognized by the receive-pack (push to server) process. +The 'report-status', 'delete-refs', 'quiet', and 'push-cert' capabilities +are sent and recognized by the receive-pack (push to server) process. The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized by both upload-pack and receive-pack protocols. The 'agent' capability @@ -250,3 +250,11 @@ allow-tip-sha1-in-want If the upload-pack server advertises this capability, fetch-pack may send "want" lines with SHA-1s that exist at the server but are not advertised by upload-pack. + +push-cert +--------- + +The receive-pack server that advertises this capability is willing +to accept a signed push certificate. A send-pack client MUST NOT +send a push-cert packet unless the receive-pack server advertises +this capability. diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index c0a3189943..431af39335 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -926,6 +926,28 @@ static struct command **queue_command(struct command **tail, 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; @@ -981,6 +1003,10 @@ static struct command *read_head_info(struct sha1_array *shallow) p = queue_command(p, line, linelen); } + + if (push_cert.len) + queue_commands_from_cert(p, &push_cert); + return commands; } diff --git a/send-pack.c b/send-pack.c index d392f5b3a0..857beb393d 100644 --- a/send-pack.c +++ b/send-pack.c @@ -363,7 +363,7 @@ int send_pack(struct send_pack_args *args, for (ref = remote_refs; ref; ref = ref->next) { char *old_hex, *new_hex; - if (args->dry_run) + if (args->dry_run || args->push_cert) continue; if (!ref_update_to_be_sent(ref, args)) -- cgit v1.2.3 From b89363e4a5277038629491f8765c0598f366326c Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 21 Aug 2014 16:45:30 -0700 Subject: signed push: fortify against replay attacks In order to prevent a valid push certificate for pushing into an repository from getting replayed in a different push operation, send a nonce string from the receive-pack process and have the signer include it in the push certificate. The receiving end uses an HMAC hash of the path to the repository it serves and the current time stamp, hashed with a secret seed (the secret seed does not have to be per-repository but can be defined in /etc/gitconfig) to generate the nonce, in order to ensure that a random third party cannot forge a nonce that looks like it originated from it. The original nonce is exported as GIT_PUSH_CERT_NONCE for the hooks to examine and match against the value on the "nonce" header in the certificate to notice a replay, but returned "nonce" header in the push certificate is examined by receive-pack and the result is exported as GIT_PUSH_CERT_NONCE_STATUS, whose value would be "OK" if the nonce recorded in the certificate matches what we expect, so that the hooks can more easily check. Signed-off-by: Junio C Hamano --- Documentation/config.txt | 12 +- Documentation/git-receive-pack.txt | 19 ++++ Documentation/technical/pack-protocol.txt | 6 + Documentation/technical/protocol-capabilities.txt | 7 +- builtin/receive-pack.c | 132 ++++++++++++++++++++-- send-pack.c | 18 ++- t/t5534-push-signed.sh | 22 ++-- 7 files changed, 187 insertions(+), 29 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/Documentation/config.txt b/Documentation/config.txt index 0d01e32888..dd6fd65e9f 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2038,17 +2038,17 @@ rebase.autostash:: successful rebase might result in non-trivial conflicts. Defaults to false. -receive.acceptpushcert:: - By default, `git receive-pack` will advertise that it - accepts `git push --signed`. Setting this variable to - false disables it (this is a tentative variable that - will go away at the end of this series). - receive.autogc:: By default, git-receive-pack will run "git-gc --auto" after receiving data from git-push and updating refs. You can stop it by setting this variable to false. +receive.certnonceseed:: + By setting this variable to a string, `git receive-pack` + will accept a `git push --signed` and verifies it by using + a "nonce" protected by HMAC using this string as a secret + key. + receive.fsckObjects:: If it is set to true, git-receive-pack will check all received objects. It will abort in the case of a malformed object or a diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt index e6df234926..2d4b45242c 100644 --- a/Documentation/git-receive-pack.txt +++ b/Documentation/git-receive-pack.txt @@ -72,6 +72,24 @@ the following environment variables: using the same mnemonic as used in `%G?` format of `git log` family of commands (see linkgit:git-log[1]). +`GIT_PUSH_CERT_NONCE`:: + The nonce string the process asked the signer to include + in the push certificate. If this does not match the value + recorded on the "nonce" header in the push certificate, it + may indicate that the certificate is a valid one that is + being replayed from a separate "git push" session. + +`GIT_PUSH_CERT_NONCE_STATUS`:: +`UNSOLICITED`;; + "git push --signed" sent a nonce when we did not ask it to + send one. +`MISSING`;; + "git push --signed" did not send any nonce header. +`BAD`;; + "git push --signed" sent a bogus nonce. +`OK`;; + "git push --signed" sent the nonce we asked it to send. + This hook is called before any refname is updated and before any fast-forward checks are performed. @@ -147,6 +165,7 @@ service: if test -n "${GIT_PUSH_CERT-}" && test ${GIT_PUSH_CERT_STATUS} = G then ( + echo expected nonce is ${GIT_PUSH_NONCE} git cat-file blob ${GIT_PUSH_CERT} ) | mail -s "push certificate from $GIT_PUSH_CERT_SIGNER" push-log@mydomain fi diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt index 7b543dc311..dda120631e 100644 --- a/Documentation/technical/pack-protocol.txt +++ b/Documentation/technical/pack-protocol.txt @@ -485,6 +485,7 @@ references. PKT-LINE("certificate version 0.1" LF) PKT-LINE("pusher" SP ident LF) PKT-LINE("pushee" SP url LF) + PKT-LINE("nonce" SP nonce LF) PKT-LINE(LF) *PKT-LINE(command LF) *PKT-LINE(gpg-signature-lines LF) @@ -533,6 +534,11 @@ Currently, the following header fields are defined: authentication material) the user who ran `git push` intended to push into. +`nonce` nonce:: + The 'nonce' string the receiving repository asked the + pushing user to include in the certificate, to prevent + replay attacks. + The GPG signature lines are a detached signature for the contents recorded in the push certificate before the signature block begins. The detached signature is used to certify that the commands were diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt index a478cc4135..0c92deebcc 100644 --- a/Documentation/technical/protocol-capabilities.txt +++ b/Documentation/technical/protocol-capabilities.txt @@ -251,10 +251,11 @@ If the upload-pack server advertises this capability, fetch-pack may send "want" lines with SHA-1s that exist at the server but are not advertised by upload-pack. -push-cert ---------- +push-cert= +----------------- The receive-pack server that advertises this capability is willing -to accept a signed push certificate. A send-pack client MUST NOT +to accept a signed push certificate, and asks the to be +included in the push certificate. A send-pack client MUST NOT send a push-cert packet unless the receive-pack server advertises this capability. diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 431af39335..91d1a6f59d 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -48,10 +48,17 @@ static void *head_name_to_free; static int sent_capabilities; static int shallow_update; 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 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_status; static enum deny_action parse_deny_action(const char *var, const char *value) { @@ -135,10 +142,8 @@ static int receive_pack_config(const char *var, const char *value, void *cb) return 0; } - if (strcmp(var, "receive.acceptpushcert") == 0) { - accept_push_cert = git_config_bool(var, value); - return 0; - } + if (strcmp(var, "receive.certnonceseed") == 0) + return git_config_string(&cert_nonce_seed, var, value); return git_default_config(var, value, cb); } @@ -157,8 +162,8 @@ static void show_ref(const char *path, const unsigned char *sha1) "report-status delete-refs side-band-64k quiet"); if (prefer_ofs_delta) strbuf_addstr(&cap, " ofs-delta"); - if (accept_push_cert) - strbuf_addstr(&cap, " push-cert"); + 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); @@ -271,6 +276,110 @@ 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[20], + 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, sizeof(out)); + 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"); + 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; + } + + /* returned nonce MUST match what we gave out earlier */ + retval = NONCE_BAD; + +leave: + free(nonce); + return retval; +} + static void prepare_push_cert_sha1(struct child_process *proc) { static int already_done; @@ -305,6 +414,7 @@ static void prepare_push_cert_sha1(struct child_process *proc) 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)); @@ -313,7 +423,10 @@ static void prepare_push_cert_sha1(struct child_process *proc) 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); + } proc->env = env.argv; } } @@ -1296,6 +1409,8 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) die("'%s' does not appear to be a git repository", dir); git_config(receive_pack_config, NULL); + if (cert_nonce_seed) + push_cert_nonce = prepare_push_cert_nonce(dir, time(NULL)); if (0 <= transfer_unpack_limit) unpack_limit = transfer_unpack_limit; @@ -1340,5 +1455,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; } diff --git a/send-pack.c b/send-pack.c index 9c2c64966d..7ad1a5968b 100644 --- a/send-pack.c +++ b/send-pack.c @@ -228,7 +228,8 @@ static const char *next_line(const char *line, size_t len) static int generate_push_cert(struct strbuf *req_buf, const struct ref *remote_refs, struct send_pack_args *args, - const char *cap_string) + const char *cap_string, + const char *push_cert_nonce) { const struct ref *ref; char stamp[60]; @@ -245,6 +246,8 @@ static int generate_push_cert(struct strbuf *req_buf, strbuf_addf(&cert, "pushee %s\n", anon_url); free(anon_url); } + if (push_cert_nonce[0]) + strbuf_addf(&cert, "nonce %s\n", push_cert_nonce); strbuf_addstr(&cert, "\n"); for (ref = remote_refs; ref; ref = ref->next) { @@ -295,6 +298,7 @@ int send_pack(struct send_pack_args *args, unsigned cmds_sent = 0; int ret; struct async demux; + const char *push_cert_nonce = NULL; /* Does the other end support the reporting? */ if (server_supports("report-status")) @@ -311,8 +315,14 @@ int send_pack(struct send_pack_args *args, agent_supported = 1; if (server_supports("no-thin")) args->use_thin_pack = 0; - if (args->push_cert && !server_supports("push-cert")) - die(_("the receiving end does not support --signed push")); + if (args->push_cert) { + int len; + + push_cert_nonce = server_feature_value("push-cert", &len); + if (!push_cert_nonce) + die(_("the receiving end does not support --signed push")); + push_cert_nonce = xmemdupz(push_cert_nonce, len); + } if (!remote_refs) { fprintf(stderr, "No refs in common and none specified; doing nothing.\n" @@ -343,7 +353,7 @@ int send_pack(struct send_pack_args *args, if (!args->dry_run && args->push_cert) cmds_sent = generate_push_cert(&req_buf, remote_refs, args, - cap_buf.buf); + cap_buf.buf, push_cert_nonce); /* * Clear the status for each ref and see if we need to send diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh index 2f4b74ed83..2786346f9a 100755 --- a/t/t5534-push-signed.sh +++ b/t/t5534-push-signed.sh @@ -50,7 +50,6 @@ test_expect_success 'unsigned push does not send push certificate' ' test_expect_success 'talking with a receiver without push certificate support' ' prepare_dst && mkdir -p dst/.git/hooks && - git -C dst config receive.acceptpushcert no && write_script dst/.git/hooks/post-receive <<-\EOF && # discard the update list cat >/dev/null @@ -68,7 +67,6 @@ test_expect_success 'talking with a receiver without push certificate support' ' test_expect_success 'push --signed fails with a receiver without push certificate support' ' prepare_dst && mkdir -p dst/.git/hooks && - git -C dst config receive.acceptpushcert no && test_must_fail git push --signed dst noop ff +noff 2>err && test_i18ngrep "the receiving end does not support" err ' @@ -89,6 +87,7 @@ test_expect_success GPG 'no certificate for a signed push with no update' ' test_expect_success GPG 'signed push sends push certificate' ' prepare_dst && mkdir -p dst/.git/hooks && + git -C dst config receive.certnonceseed sekrit && write_script dst/.git/hooks/post-receive <<-\EOF && # discard the update list cat >/dev/null @@ -102,17 +101,24 @@ test_expect_success GPG 'signed push sends push certificate' ' 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 - cat >expect <<-\EOF && - SIGNER=C O Mitter - KEY=13B6F51ECDDE430D - STATUS=G - EOF - git push --signed dst noop ff +noff && + + ( + cat <<-\EOF && + SIGNER=C O Mitter + KEY=13B6F51ECDDE430D + STATUS=G + NONCE_STATUS=OK + EOF + sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" dst/push-cert + ) >expect && + grep "$(git rev-parse noop ff) refs/heads/ff" dst/push-cert && grep "$(git rev-parse noop noff) refs/heads/noff" dst/push-cert && test_cmp expect dst/push-cert-status -- cgit v1.2.3 From 5732373daacf9486a0db9741cf0de4e7a41b08b3 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 5 Sep 2014 10:46:04 -0700 Subject: signed push: allow stale nonce in stateless mode When operating with the stateless RPC mode, we will receive a nonce issued by another instance of us that advertised our capability and refs some time ago. Update the logic to check received nonce to detect this case, compute how much time has passed since the nonce was issued and report the status with a new environment variable GIT_PUSH_CERT_NONCE_SLOP to the hooks. GIT_PUSH_CERT_NONCE_STATUS will report "SLOP" in such a case. The hooks are free to decide how large a slop it is willing to accept. Strictly speaking, the "nonce" is not really a "nonce" anymore in the stateless RPC mode, as it will happily take any "nonce" issued by it (which is protected by HMAC and its secret key) as long as it is fresh enough. The degree of this security degradation, relative to the native protocol, is about the same as the "we make sure that the 'git push' decided to update our refs with new objects based on the freshest observation of our refs by making sure the values they claim the original value of the refs they ask us to update exactly match the current state" security is loosened to accomodate the stateless RPC mode in the existing code without this series, so there is no need for those who are already using smart HTTP to push to their repositories to be alarmed any more than they already are. In addition, the server operator can set receive.certnonceslop configuration variable to specify how stale a nonce can be (in seconds). When this variable is set, and if the nonce received in the certificate that passes the HMAC check was less than that many seconds old, hooks are given "OK" in GIT_PUSH_CERT_NONCE_STATUS (instead of "SLOP") and the received nonce value is given in GIT_PUSH_CERT_NONCE, which makes it easier for a simple-minded hook to check if the certificate we received is recent enough. Signed-off-by: Junio C Hamano --- Documentation/config.txt | 13 ++++++ Documentation/git-receive-pack.txt | 13 ++++++ builtin/receive-pack.c | 89 +++++++++++++++++++++++++++++++++----- t/t5541-http-push-smart.sh | 9 +++- 4 files changed, 112 insertions(+), 12 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/Documentation/config.txt b/Documentation/config.txt index dd6fd65e9f..d73366f6b8 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2049,6 +2049,19 @@ receive.certnonceseed:: a "nonce" protected by HMAC using this string as a secret key. +receive.certnonceslop:: + When a `git push --signed` sent a push certificate with a + "nonce" that was issued by a receive-pack serving the same + repository within this many seconds, export the "nonce" + found in the certificate to `GIT_PUSH_CERT_NONCE` to the + hooks (instead of what the receive-pack asked the sending + side to include). This may allow writing checks in + `pre-receive` and `post-receive` a bit easier. Instead of + checking `GIT_PUSH_CERT_NONCE_SLOP` environment variable + that records by how many seconds the nonce is stale to + decide if they want to accept the certificate, they only + can check `GIT_PUSH_CERT_NONCE_STATUS` is `OK`. + receive.fsckObjects:: If it is set to true, git-receive-pack will check all received objects. It will abort in the case of a malformed object or a diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt index 2d4b45242c..9016960e27 100644 --- a/Documentation/git-receive-pack.txt +++ b/Documentation/git-receive-pack.txt @@ -89,6 +89,19 @@ the following environment variables: "git push --signed" sent a bogus nonce. `OK`;; "git push --signed" sent the nonce we asked it to send. +`SLOP`;; + "git push --signed" sent a nonce different from what we + asked it to send now, but in a previous session. See + `GIT_PUSH_CERT_NONCE_SLOP` environment variable. + +`GIT_PUSH_CERT_NONCE_SLOP`:: + "git push --signed" sent a nonce different from what we + asked it to send now, but in a different session whose + starting time is different by this many seconds from the + current session. Only meaningful when + `GIT_PUSH_CERT_NONCE_STATUS` says `SLOP`. + Also read about `receive.certnonceslop` variable in + linkgit:git-config[1]. This hook is called before any refname is updated and before any fast-forward checks are performed. diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 91d1a6f59d..efb13b1134 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -43,6 +43,8 @@ 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; @@ -58,7 +60,10 @@ 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) { @@ -145,6 +150,11 @@ static int receive_pack_config(const char *var, const char *value, void *cb) 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); } @@ -359,6 +369,8 @@ static char *find_header(const char *msg, size_t len, const char *key) 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) { @@ -372,11 +384,67 @@ static const char *check_nonce(const char *buf, size_t len) goto leave; } - /* returned nonce MUST match what we gave out earlier */ - retval = NONCE_BAD; + 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(, "-", ) */ + 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; } @@ -426,6 +494,9 @@ static void prepare_push_cert_sha1(struct child_process *proc) 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; } @@ -1361,9 +1432,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; @@ -1396,21 +1465,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(dir, time(NULL)); + push_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL)); if (0 <= transfer_unpack_limit) unpack_limit = transfer_unpack_limit; diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh index 24926a4a42..ffb3af4498 100755 --- a/t/t5541-http-push-smart.sh +++ b/t/t5541-http-push-smart.sh @@ -340,21 +340,26 @@ test_expect_success GPG 'push with post-receive to inspect certificate' ' 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 - git config receive.certnonceseed sekrit + git config receive.certnonceseed sekrit && + git config receive.certnonceslop 30 ) && cd "$ROOT_PATH/test_repo_clone" && test_commit cert-test && git push --signed "$HTTPD_URL/smart/test_repo.git" && ( cd "$HTTPD_DOCUMENT_ROOT_PATH" && - cat <<-\EOF + cat <<-\EOF && SIGNER=C O Mitter KEY=13B6F51ECDDE430D STATUS=G + NONCE_STATUS=OK EOF + sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" push-cert ) >expect && test_cmp expect "$HTTPD_DOCUMENT_ROOT_PATH/push-cert-status" ' -- cgit v1.2.3 From 6f5ef44e0d8933621fcd50127518557013002313 Mon Sep 17 00:00:00 2001 From: Brian Gernhardt Date: Thu, 25 Sep 2014 11:02:20 -0400 Subject: receive-pack::hmac_sha1(): copy the entire SHA-1 hash out clang gives the following warning: builtin/receive-pack.c:327:35: error: sizeof on array function parameter will return size of 'unsigned char *' instead of 'unsigned char [20]' [-Werror,-Wsizeof-array-argument] git_SHA1_Update(&ctx, out, sizeof(out)); ^ builtin/receive-pack.c:292:37: note: declared here static void hmac_sha1(unsigned char out[20], ^ Signed-off-by: Brian Gernhardt Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'builtin/receive-pack.c') diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index efb13b1134..42f25a5103 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -288,7 +288,7 @@ static int copy_to_sideband(int in, int out, void *arg) #define HMAC_BLOCK_SIZE 64 -static void hmac_sha1(unsigned char out[20], +static void hmac_sha1(unsigned char *out, const char *key_in, size_t key_len, const char *text, size_t text_len) { @@ -323,7 +323,7 @@ static void hmac_sha1(unsigned char out[20], /* RFC 2104 2. (6) & (7) */ git_SHA1_Init(&ctx); git_SHA1_Update(&ctx, k_opad, sizeof(k_opad)); - git_SHA1_Update(&ctx, out, sizeof(out)); + git_SHA1_Update(&ctx, out, 20); git_SHA1_Final(out, &ctx); } -- cgit v1.2.3