Merge branch 'jc/push-cert'
authorJunio C Hamano <gitster@pobox.com>
Wed, 8 Oct 2014 20:05:15 +0000 (13:05 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 8 Oct 2014 20:05:25 +0000 (13:05 -0700)
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
...

13 files changed:
1  2 
Documentation/config.txt
Documentation/git-push.txt
Documentation/technical/pack-protocol.txt
builtin/receive-pack.c
builtin/send-pack.c
commit.c
gpg-interface.c
remote-curl.c
send-pack.c
t/t5541-http-push-smart.sh
t/test-lib.sh
transport-helper.c
transport.c
Simple merge
Simple merge
index 569c48a352b76ab3fc0d31f21f0f1a1bd654f41d,dda120631e78a63dd973bc14c799455be71006b3..462e20645f1ea87dcc04938b8ca504bd0d7d7636
@@@ -465,9 -465,9 +465,9 @@@ contain all the objects that the serve
  references.
  
  ----
-   update-request    =  *shallow command-list [pack-file]
+   update-request    =  *shallow ( command-list | push-cert ) [pack-file]
  
 -  shallow           =  PKT-LINE("shallow" SP obj-id)
 +  shallow           =  PKT-LINE("shallow" SP obj-id LF)
  
    command-list      =  PKT-LINE(command NUL capability-list LF)
                       *PKT-LINE(command LF)
index daf0600ca30eb3969e0583cc1096e6f33291d4aa,42f25a5103a72486c46d7b3a584f249495dc12b4..a01ac2096a70fcfbe4f206cca1b06ded1a1daffc
@@@ -15,7 -15,8 +15,9 @@@
  #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>";
  
Simple merge
diff --cc commit.c
index 9c4439fed61c3b26d493d0f0306b65a471da9b19,01cdad2626f34cbab87a12d0b39e36fcc720f725..19cf8f9c67bd0f4d71172f33424f7d2371a3af68
+++ b/commit.c
@@@ -1214,43 -1220,7 +1214,7 @@@ free_return
        free(buf);
  }
  
- static struct {
-       char result;
-       const char *check;
- } sigcheck_gpg_status[] = {
-       { 'G', "\n[GNUPG:] GOODSIG " },
-       { 'B', "\n[GNUPG:] BADSIG " },
-       { 'U', "\n[GNUPG:] TRUST_NEVER" },
-       { 'U', "\n[GNUPG:] TRUST_UNDEFINED" },
- };
- static void parse_gpg_output(struct signature_check *sigc)
- {
-       const char *buf = sigc->gpg_status;
-       int i;
-       /* Iterate over all search strings */
-       for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {
-               const char *found, *next;
-               if (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) {
-                       found = strstr(buf, sigcheck_gpg_status[i].check);
-                       if (!found)
-                               continue;
-                       found += strlen(sigcheck_gpg_status[i].check);
-               }
-               sigc->result = sigcheck_gpg_status[i].result;
-               /* The trust messages are not followed by key/signer information */
-               if (sigc->result != 'U') {
-                       sigc->key = xmemdupz(found, 16);
-                       found += 17;
-                       next = strchrnul(found, '\n');
-                       sigc->signer = xmemdupz(found, next - found);
-               }
-       }
- }
 -void check_commit_signature(const struct commit* commit, struct signature_check *sigc)
 +void check_commit_signature(const struct commit *commit, struct signature_check *sigc)
  {
        struct strbuf payload = STRBUF_INIT;
        struct strbuf signature = STRBUF_INIT;
diff --cc gpg-interface.c
Simple merge
diff --cc remote-curl.c
Simple merge
diff --cc send-pack.c
index 8b4cbf049c243b8cdc1add94ddf1bf50bcbd8df9,7ad1a5968b6c86598edee62a0b0c35860f7e9c50..949cb61aa0d681e30b7e8199ef9e31ff0caee089
@@@ -189,6 -191,94 +190,94 @@@ static void advertise_shallow_grafts_bu
        for_each_commit_graft(advertise_shallow_grafts_cb, sb);
  }
  
 -      char stamp[60];
+ static int ref_update_to_be_sent(const struct ref *ref, const struct send_pack_args *args)
+ {
+       if (!ref->peer_ref && !args->send_mirror)
+               return 0;
+       /* Check for statuses set by set_ref_status_for_push() */
+       switch (ref->status) {
+       case REF_STATUS_REJECT_NONFASTFORWARD:
+       case REF_STATUS_REJECT_ALREADY_EXISTS:
+       case REF_STATUS_REJECT_FETCH_FIRST:
+       case REF_STATUS_REJECT_NEEDS_FORCE:
+       case REF_STATUS_REJECT_STALE:
+       case REF_STATUS_REJECT_NODELETE:
+       case REF_STATUS_UPTODATE:
+               return 0;
+       default:
+               return 1;
+       }
+ }
+ /*
+  * 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 int generate_push_cert(struct strbuf *req_buf,
+                             const struct ref *remote_refs,
+                             struct send_pack_args *args,
+                             const char *cap_string,
+                             const char *push_cert_nonce)
+ {
+       const struct ref *ref;
 -      datestamp(stamp, sizeof(stamp));
+       char *signing_key = xstrdup(get_signing_key());
+       const char *cp, *np;
+       struct strbuf cert = STRBUF_INIT;
+       int update_seen = 0;
 -      strbuf_addf(&cert, "pusher %s %s\n", signing_key, stamp);
+       strbuf_addf(&cert, "certificate version 0.1\n");
++      strbuf_addf(&cert, "pusher %s ", signing_key);
++      datestamp(&cert);
++      strbuf_addch(&cert, '\n');
+       if (args->url && *args->url) {
+               char *anon_url = transport_anonymize_url(args->url);
+               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) {
+               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%c%s", 0, cap_string);
+       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);
+       return update_seen;
+ }
  int send_pack(struct send_pack_args *args,
              int fd[], struct child_process *conn,
              struct ref *remote_refs,
index db1998873cee18a74d98d13963ad640bc1f8abbe,ffb3af44984ee4b22dcda50f4f3947d96c95e138..d2c681ebfde39fcccef190c3a242dfae9d8af2f2
@@@ -323,20 -324,45 +324,60 @@@ test_expect_success 'push into half-aut
        test_cmp expect actual
  '
  
 +run_with_limited_cmdline () {
 +      (ulimit -s 128 && "$@")
 +}
 +
 +test_lazy_prereq CMDLINE_LIMIT 'run_with_limited_cmdline true'
 +
 +test_expect_success CMDLINE_LIMIT 'push 2000 tags over http' '
 +      sha1=$(git rev-parse HEAD) &&
 +      test_seq 2000 |
 +        sort |
 +        sed "s|.*|$sha1 refs/tags/really-long-tag-name-&|" \
 +        >.git/packed-refs &&
 +      run_with_limited_cmdline git push --mirror
 +'
 +
+ test_expect_success GPG 'push with post-receive to inspect certificate' '
+       (
+               cd "$HTTPD_DOCUMENT_ROOT_PATH"/test_repo.git &&
+               mkdir -p hooks &&
+               write_script hooks/post-receive <<-\EOF &&
+               # discard the update list
+               cat >/dev/null
+               # record the push certificate
+               if test -n "${GIT_PUSH_CERT-}"
+               then
+                       git cat-file blob $GIT_PUSH_CERT >../push-cert
+               fi &&
+               cat >../push-cert-status <<E_O_F
+               SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}
+               KEY=${GIT_PUSH_CERT_KEY-nokey}
+               STATUS=${GIT_PUSH_CERT_STATUS-nostatus}
+               NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}
+               NONCE=${GIT_PUSH_CERT_NONCE-nononce}
+               E_O_F
+               EOF
+               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 &&
+               SIGNER=C O Mitter <committer@example.com>
+               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"
+ '
  stop_httpd
  test_done
diff --cc t/test-lib.sh
Simple merge
Simple merge
diff --cc transport.c
Simple merge