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
diff --combined Documentation/config.txt
index 3b5b24aeb7f16c03e06bd6f38fd89e9080011ce7,d73366f6b877ad25a2b968d7740212a545c5285b..04a1e2f37e938f004c899d3fb6c59d282c2071ce
@@@ -499,8 -499,7 +499,8 @@@ core.bigFileThreshold:
        Files larger than this size are stored deflated, without
        attempting delta compression.  Storing large files without
        delta compression avoids excessive memory usage, at the
 -      slight expense of increased disk usage.
 +      slight expense of increased disk usage. Additionally files
 +      larger than this size are always treated as binary.
  +
  Default is 512 MiB on all platforms.  This should be reasonable
  for most projects as source code and other text files can still
@@@ -2044,6 -2043,25 +2044,25 @@@ receive.autogc:
        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.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
index c0d7403b9a144292f1bb8e4eb6b83da25086f0ee,21b3f29c3bc603df74e07226d27ad63faa6fae24..b17283ab7a1cc73c5ec741e0da1128bea2a57a65
@@@ -10,7 -10,8 +10,8 @@@ SYNOPSI
  --------
  [verse]
  'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
-          [--repo=<repository>] [-f | --force] [--prune] [-v | --verbose] [-u | --set-upstream]
+          [--repo=<repository>] [-f | --force] [--prune] [-v | --verbose]
+          [-u | --set-upstream] [--signed]
           [--force-with-lease[=<refname>[:<expect>]]]
           [--no-verify] [<repository> [<refspec>...]]
  
@@@ -33,7 -34,7 +34,7 @@@ When the command line does not specify 
  arguments or `--all`, `--mirror`, `--tags` options, the command finds
  the default `<refspec>` by consulting `remote.*.push` configuration,
  and if it is not found, honors `push.default` configuration to decide
 -what to push (See gitlink:git-config[1] for the meaning of `push.default`).
 +what to push (See linkgit:git-config[1] for the meaning of `push.default`).
  
  
  OPTIONS[[OPTIONS]]
@@@ -129,6 -130,12 +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=<git-receive-pack>::
  --exec=<git-receive-pack>::
        Path to the 'git-receive-pack' program on the remote
index 569c48a352b76ab3fc0d31f21f0f1a1bd654f41d,dda120631e78a63dd973bc14c799455be71006b3..462e20645f1ea87dcc04938b8ca504bd0d7d7636
@@@ -212,9 -212,9 +212,9 @@@ out of what the server said it could d
    want-list         =  first-want
                       *additional-want
  
-   shallow-line      =  PKT_LINE("shallow" SP obj-id)
+   shallow-line      =  PKT-LINE("shallow" SP obj-id)
  
-   depth-request     =  PKT_LINE("deepen" SP depth)
+   depth-request     =  PKT-LINE("deepen" SP depth)
  
    first-want        =  PKT-LINE("want" SP obj-id SP capability-list LF)
    additional-want   =  PKT-LINE("want" SP obj-id LF)
@@@ -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)
    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("pushee" SP url LF)
+                     PKT-LINE("nonce" SP nonce 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 -516,34 +516,34 @@@ was being processed (the obj-id is stil
  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 <email@address>"
+       format.
+ `pushee` url::
+       The repository URL (anonymized, if the URL contains
+       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
+ given by the pusher, who must be the signer.
  Report Status
  -------------
  
diff --combined builtin/receive-pack.c
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>";
  
@@@ -42,11 -43,27 +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 -147,14 +148,14 @@@ static int receive_pack_config(const ch
                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 -163,23 +164,23 @@@ static void show_ref(const char *path, 
        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,10 -286,226 +287,226 @@@ static int copy_to_sideband(int in, in
        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)
  {
 -      struct child_process proc;
 +      struct child_process proc = CHILD_PROCESS_INIT;
        struct async muxer;
        const char *argv[2];
        int code;
  
        argv[1] = NULL;
  
 -      memset(&proc, 0, sizeof(proc));
        proc.argv = argv;
        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;
                return code;
        }
  
 +      sigchain_push(SIGPIPE, SIG_IGN);
 +
        while (1) {
                const char *buf;
                size_t n;
        close(proc.in);
        if (use_sideband)
                finish_async(&muxer);
 +
 +      sigchain_pop(SIGPIPE);
 +
        return finish_command(&proc);
  }
  
@@@ -355,7 -602,7 +607,7 @@@ static int run_receive_hook(struct comm
  static int run_update_hook(struct command *cmd)
  {
        const char *argv[5];
 -      struct child_process proc;
 +      struct child_process proc = CHILD_PROCESS_INIT;
        int code;
  
        argv[0] = find_hook("update");
        argv[3] = sha1_to_hex(cmd->new_sha1);
        argv[4] = NULL;
  
 -      memset(&proc, 0, sizeof(proc));
        proc.no_stdin = 1;
        proc.stdout_to_stderr = 1;
        proc.err = use_sideband ? -1 : 0;
@@@ -479,6 -727,7 +731,6 @@@ static const char *update(struct comman
        const char *namespaced_name;
        unsigned char *old_sha1 = cmd->old_sha1;
        unsigned char *new_sha1 = cmd->new_sha1;
 -      struct ref_lock *lock;
  
        /* only refs/... are allowed */
        if (!starts_with(name, "refs/") || check_refname_format(name + 5, 0)) {
                return NULL; /* good */
        }
        else {
 +              struct strbuf err = STRBUF_INIT;
 +              struct ref_transaction *transaction;
 +
                if (shallow_update && si->shallow_ref[cmd->index] &&
                    update_shallow_ref(cmd, si))
                        return "shallow error";
  
 -              lock = lock_any_ref_for_update(namespaced_name, old_sha1,
 -                                             0, NULL);
 -              if (!lock) {
 -                      rp_error("failed to lock %s", name);
 -                      return "failed to lock";
 -              }
 -              if (write_ref_sha1(lock, new_sha1, "push")) {
 -                      return "failed to write"; /* error() already called */
 +              transaction = ref_transaction_begin(&err);
 +              if (!transaction ||
 +                  ref_transaction_update(transaction, namespaced_name,
 +                                         new_sha1, old_sha1, 0, 1, &err) ||
 +                  ref_transaction_commit(transaction, "push", &err)) {
 +                      ref_transaction_free(transaction);
 +
 +                      rp_error("%s", err.buf);
 +                      strbuf_release(&err);
 +                      return "failed to update ref";
                }
 +
 +              ref_transaction_free(transaction);
 +              strbuf_release(&err);
                return NULL; /* good */
        }
  }
@@@ -609,7 -850,7 +861,7 @@@ static void run_update_post_hook(struc
        struct command *cmd;
        int argc;
        const char **argv;
 -      struct child_process proc;
 +      struct child_process proc = CHILD_PROCESS_INIT;
        char *hook;
  
        hook = find_hook("post-update");
        }
        argv[argc] = NULL;
  
 -      memset(&proc, 0, sizeof(proc));
        proc.no_stdin = 1;
        proc.stdout_to_stderr = 1;
        proc.err = use_sideband ? -1 : 0;
@@@ -841,40 -1083,79 +1093,79 @@@ static void execute_commands(struct com
                      "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"))
                        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;
  }
  
@@@ -921,7 -1223,7 +1233,7 @@@ static const char *unpack(int err_fd, s
        const char *hdr_err;
        int status;
        char hdr_arg[38];
 -      struct child_process child;
 +      struct child_process child = CHILD_PROCESS_INIT;
        int fsck_objects = (receive_fsck_objects >= 0
                            ? receive_fsck_objects
                            : transfer_fsck_objects >= 0
                argv_array_pushl(&av, "--shallow-file", alt_shallow_file, NULL);
        }
  
 -      memset(&child, 0, sizeof(child));
        if (ntohl(hdr.hdr_entries) < unpack_limit) {
                argv_array_pushl(&av, "unpack-objects", hdr_arg, NULL);
                if (quiet)
@@@ -1129,9 -1432,7 +1441,7 @@@ static int delete_only(struct command *
  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;
  
                        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;
                packet_flush(1);
        sha1_array_clear(&shallow);
        sha1_array_clear(&ref);
+       free((void *)push_cert_nonce);
        return 0;
  }
diff --combined builtin/send-pack.c
index 4b1bc0fef72c9764a4e4a43bb30f2c820d1aec34,ca28d8d2488a3240341e4163f06e1f76ad108b58..b564a778455c352379127c3f06349e0ebbc81c98
@@@ -110,7 -110,6 +110,7 @@@ int cmd_send_pack(int argc, const char 
        int flags;
        unsigned int reject_reasons;
        int progress = -1;
 +      int from_stdin = 0;
        struct push_cas_option cas = {0};
  
        argv++;
                                args.verbose = 1;
                                continue;
                        }
+                       if (!strcmp(arg, "--signed")) {
+                               args.push_cert = 1;
+                               continue;
+                       }
                        if (!strcmp(arg, "--progress")) {
                                progress = 1;
                                continue;
                                args.stateless_rpc = 1;
                                continue;
                        }
 +                      if (!strcmp(arg, "--stdin")) {
 +                              from_stdin = 1;
 +                              continue;
 +                      }
                        if (!strcmp(arg, "--helper-status")) {
                                helper_status = 1;
                                continue;
        }
        if (!dest)
                usage(send_pack_usage);
 +
 +      if (from_stdin) {
 +              struct argv_array all_refspecs = ARGV_ARRAY_INIT;
 +
 +              for (i = 0; i < nr_refspecs; i++)
 +                      argv_array_push(&all_refspecs, refspecs[i]);
 +
 +              if (args.stateless_rpc) {
 +                      const char *buf;
 +                      while ((buf = packet_read_line(0, NULL)))
 +                              argv_array_push(&all_refspecs, buf);
 +              } else {
 +                      struct strbuf line = STRBUF_INIT;
 +                      while (strbuf_getline(&line, stdin, '\n') != EOF)
 +                              argv_array_push(&all_refspecs, line.buf);
 +                      strbuf_release(&line);
 +              }
 +
 +              refspecs = all_refspecs.argv;
 +              nr_refspecs = all_refspecs.argc;
 +      }
 +
        /*
         * --all and --mirror are incompatible; neither makes sense
         * with any refspecs.
diff --combined commit.c
index 9c4439fed61c3b26d493d0f0306b65a471da9b19,01cdad2626f34cbab87a12d0b39e36fcc720f725..19cf8f9c67bd0f4d71172f33424f7d2371a3af68
+++ b/commit.c
@@@ -584,19 -584,25 +584,19 @@@ define_commit_slab(author_date_slab, un
  static void record_author_date(struct author_date_slab *author_date,
                               struct commit *commit)
  {
 -      const char *buf, *line_end, *ident_line;
        const char *buffer = get_commit_buffer(commit, NULL);
        struct ident_split ident;
 +      const char *ident_line;
 +      size_t ident_len;
        char *date_end;
        unsigned long date;
  
 -      for (buf = buffer; buf; buf = line_end + 1) {
 -              line_end = strchrnul(buf, '\n');
 -              if (!skip_prefix(buf, "author ", &ident_line)) {
 -                      if (!line_end[0] || line_end[1] == '\n')
 -                              return; /* end of header */
 -                      continue;
 -              }
 -              if (split_ident_line(&ident,
 -                                   ident_line, line_end - ident_line) ||
 -                  !ident.date_begin || !ident.date_end)
 -                      goto fail_exit; /* malformed "author" line */
 -              break;
 -      }
 +      ident_line = find_commit_header(buffer, "author", &ident_len);
 +      if (!ident_line)
 +              goto fail_exit; /* no author line */
 +      if (split_ident_line(&ident, ident_line, ident_len) ||
 +          !ident.date_begin || !ident.date_end)
 +              goto fail_exit; /* malformed "author" line */
  
        date = strtoul(ident.date_begin, &date_end, 10);
        if (date_end != ident.date_end)
@@@ -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;
@@@ -1654,25 -1624,3 +1618,25 @@@ void print_commit_list(struct commit_li
                printf(format, sha1_to_hex(list->item->object.sha1));
        }
  }
 +
 +const char *find_commit_header(const char *msg, const char *key, size_t *out_len)
 +{
 +      int key_len = strlen(key);
 +      const char *line = msg;
 +
 +      while (line) {
 +              const char *eol = strchrnul(line, '\n');
 +
 +              if (line == eol)
 +                      return NULL;
 +
 +              if (eol - line > key_len &&
 +                  !strncmp(line, key, key_len) &&
 +                  line[key_len] == ' ') {
 +                      *out_len = eol - line - key_len - 1;
 +                      return line + key_len + 1;
 +              }
 +              line = *eol ? eol + 1 : NULL;
 +      }
 +      return NULL;
 +}
diff --combined gpg-interface.c
index 1ef73fb7dfedd8cd5d3444433218ff177a63111b,0dd11eadb291b263ed34d7c6763f5cee6234c178..68b0c814f789f39151d380b1d318d6f80090d524
@@@ -7,6 -7,9 +7,9 @@@
  static char *configured_signing_key;
  static const char *gpg_program = "gpg";
  
+ #define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
+ #define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----"
  void signature_check_clear(struct signature_check *sigc)
  {
        free(sigc->payload);
        sigc->key = NULL;
  }
  
+ 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" },
+ };
+ 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);
+               }
+       }
+ }
+ /*
+  * Look at GPG signed content (e.g. a signed tag object), whose
+  * payload is followed by a detached signature on it.  Return the
+  * offset where the embedded detached signature begins, or the end of
+  * the data when there is no such signature.
+  */
+ size_t parse_signature(const char *buf, unsigned long size)
+ {
+       char *eol;
+       size_t len = 0;
+       while (len < size && !starts_with(buf + len, PGP_SIGNATURE) &&
+                       !starts_with(buf + len, PGP_MESSAGE)) {
+               eol = memchr(buf + len, '\n', size - len);
+               len += eol ? eol - (buf + len) + 1 : size - len;
+       }
+       return len;
+ }
  void set_signing_key(const char *key)
  {
        free(configured_signing_key);
@@@ -55,11 -112,12 +112,11 @@@ const char *get_signing_key(void
   */
  int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key)
  {
 -      struct child_process gpg;
 +      struct child_process gpg = CHILD_PROCESS_INIT;
        const char *args[4];
        ssize_t len;
        size_t i, j, bottom;
  
 -      memset(&gpg, 0, sizeof(gpg));
        gpg.argv = args;
        gpg.in = -1;
        gpg.out = -1;
@@@ -115,7 -173,7 +172,7 @@@ int verify_signed_buffer(const char *pa
                         const char *signature, size_t signature_size,
                         struct strbuf *gpg_output, struct strbuf *gpg_status)
  {
 -      struct child_process gpg;
 +      struct child_process gpg = CHILD_PROCESS_INIT;
        const char *args_gpg[] = {NULL, "--status-fd=1", "--verify", "FILE", "-", NULL};
        char path[PATH_MAX];
        int fd, ret;
                             path, strerror(errno));
        close(fd);
  
 -      memset(&gpg, 0, sizeof(gpg));
        gpg.argv = args_gpg;
        gpg.in = -1;
        gpg.out = -1;
diff --combined remote-curl.c
index cd626d15e59832e6fee74b2ca845a23a5fce77ee,1ea4e95d9a0b782606fc5ce9214ec6bed43e2154..dd63bc27abf8ae4b9e480ba1bb88942db11a0e48
@@@ -25,7 -25,8 +25,8 @@@ struct options 
                update_shallow : 1,
                followtags : 1,
                dry_run : 1,
-               thin : 1;
+               thin : 1,
+               push_cert : 1;
  };
  static struct options options;
  static struct string_list cas_options = STRING_LIST_INIT_DUP;
@@@ -106,6 -107,14 +107,14 @@@ static int set_option(const char *name
                else
                        return -1;
                return 0;
+       } else if (!strcmp(name, "pushcert")) {
+               if (!strcmp(value, "true"))
+                       options.push_cert = 1;
+               else if (!strcmp(value, "false"))
+                       options.push_cert = 0;
+               else
+                       return -1;
+               return 0;
        } else {
                return 1 /* unsupported */;
        }
@@@ -221,7 -230,7 +230,7 @@@ static int show_http_message(struct str
        return 0;
  }
  
 -static struct discoverydiscover_refs(const char *service, int for_push)
 +static struct discovery *discover_refs(const char *service, int for_push)
  {
        struct strbuf exp = STRBUF_INIT;
        struct strbuf type = STRBUF_INIT;
@@@ -623,9 -632,10 +632,9 @@@ static int rpc_service(struct rpc_stat
        const char *svc = rpc->service_name;
        struct strbuf buf = STRBUF_INIT;
        struct strbuf *preamble = rpc->stdin_preamble;
 -      struct child_process client;
 +      struct child_process client = CHILD_PROCESS_INIT;
        int err = 0;
  
 -      memset(&client, 0, sizeof(client));
        client.in = -1;
        client.out = -1;
        client.git_cmd = 1;
@@@ -862,7 -872,6 +871,7 @@@ static int push_git(struct discovery *h
        int i, err;
        struct argv_array args;
        struct string_list_item *cas_option;
 +      struct strbuf preamble = STRBUF_INIT;
  
        argv_array_init(&args);
        argv_array_pushl(&args, "send-pack", "--stateless-rpc", "--helper-status",
                argv_array_push(&args, "--thin");
        if (options.dry_run)
                argv_array_push(&args, "--dry-run");
+       if (options.push_cert)
+               argv_array_push(&args, "--signed");
        if (options.verbosity == 0)
                argv_array_push(&args, "--quiet");
        else if (options.verbosity > 1)
        for_each_string_list_item(cas_option, &cas_options)
                argv_array_push(&args, cas_option->string);
        argv_array_push(&args, url.buf);
 +
 +      argv_array_push(&args, "--stdin");
        for (i = 0; i < nr_spec; i++)
 -              argv_array_push(&args, specs[i]);
 +              packet_buf_write(&preamble, "%s\n", specs[i]);
 +      packet_buf_flush(&preamble);
  
        memset(&rpc, 0, sizeof(rpc));
        rpc.service_name = "git-receive-pack",
        rpc.argv = args.argv;
 +      rpc.stdin_preamble = &preamble;
  
        err = rpc_service(&rpc, heads);
        if (rpc.result.len)
                write_or_die(1, rpc.result.buf, rpc.result.len);
        strbuf_release(&rpc.result);
 +      strbuf_release(&preamble);
        argv_array_clear(&args);
        return err;
  }
diff --combined send-pack.c
index 8b4cbf049c243b8cdc1add94ddf1bf50bcbd8df9,7ad1a5968b6c86598edee62a0b0c35860f7e9c50..949cb61aa0d681e30b7e8199ef9e31ff0caee089
@@@ -11,6 -11,7 +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)
  {
@@@ -47,7 -48,7 +48,7 @@@ static int pack_objects(int fd, struct 
                NULL,
                NULL,
        };
 -      struct child_process po;
 +      struct child_process po = CHILD_PROCESS_INIT;
        int i;
  
        i = 4;
@@@ -59,6 -60,7 +60,6 @@@
                argv[i++] = "-q";
        if (args->progress)
                argv[i++] = "--progress";
 -      memset(&po, 0, sizeof(po));
        po.argv = argv;
        po.in = -1;
        po.out = args->stateless_rpc ? -1 : fd;
@@@ -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,
        int in = fd[0];
        int out = fd[1];
        struct strbuf req_buf = STRBUF_INIT;
+       struct strbuf cap_buf = STRBUF_INIT;
        struct ref *ref;
-       int new_refs;
+       int need_pack_data = 0;
        int allow_deleting_refs = 0;
        int status_report = 0;
        int use_sideband = 0;
        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"))
                agent_supported = 1;
        if (server_supports("no-thin"))
                args->use_thin_pack = 0;
+       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"
                return 0;
        }
  
+       if (status_report)
+               strbuf_addstr(&cap_buf, " report-status");
+       if (use_sideband)
+               strbuf_addstr(&cap_buf, " side-band-64k");
+       if (quiet_supported && (args->quiet || !args->progress))
+               strbuf_addstr(&cap_buf, " quiet");
+       if (agent_supported)
+               strbuf_addf(&cap_buf, " agent=%s", git_user_agent_sanitized());
+       /*
+        * NEEDSWORK: why does delete-refs have to be so specific to
+        * send-pack machinery that set_ref_status_for_push() cannot
+        * set this bit for us???
+        */
+       for (ref = remote_refs; ref; ref = ref->next)
+               if (ref->deletion && !allow_deleting_refs)
+                       ref->status = REF_STATUS_REJECT_NODELETE;
        if (!args->dry_run)
                advertise_shallow_grafts_buf(&req_buf);
  
+       if (!args->dry_run && args->push_cert)
+               cmds_sent = generate_push_cert(&req_buf, remote_refs, args,
+                                              cap_buf.buf, push_cert_nonce);
        /*
-        * Finally, tell the other end!
+        * Clear the status for each ref and see if we need to send
+        * the pack data.
         */
-       new_refs = 0;
        for (ref = remote_refs; ref; ref = ref->next) {
-               if (!ref->peer_ref && !args->send_mirror)
+               if (!ref_update_to_be_sent(ref, args))
                        continue;
  
-               /* 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_UPTODATE:
-                       continue;
-               default:
-                       ; /* do nothing */
-               }
+               if (!ref->deletion)
+                       need_pack_data = 1;
  
-               if (ref->deletion && !allow_deleting_refs) {
-                       ref->status = REF_STATUS_REJECT_NODELETE;
+               if (args->dry_run || !status_report)
+                       ref->status = REF_STATUS_OK;
+               else
+                       ref->status = REF_STATUS_EXPECTING_REPORT;
+       }
+       /*
+        * Finally, tell the other end!
+        */
+       for (ref = remote_refs; ref; ref = ref->next) {
+               char *old_hex, *new_hex;
+               if (args->dry_run || args->push_cert)
                        continue;
-               }
  
-               if (!ref->deletion)
-                       new_refs++;
+               if (!ref_update_to_be_sent(ref, args))
+                       continue;
  
-               if (args->dry_run) {
-                       ref->status = REF_STATUS_OK;
+               old_hex = sha1_to_hex(ref->old_sha1);
+               new_hex = sha1_to_hex(ref->new_sha1);
+               if (!cmds_sent) {
+                       packet_buf_write(&req_buf,
+                                        "%s %s %s%c%s",
+                                        old_hex, new_hex, ref->name, 0,
+                                        cap_buf.buf);
+                       cmds_sent = 1;
                } else {
-                       char *old_hex = sha1_to_hex(ref->old_sha1);
-                       char *new_hex = sha1_to_hex(ref->new_sha1);
-                       int quiet = quiet_supported && (args->quiet || !args->progress);
-                       if (!cmds_sent && (status_report || use_sideband ||
-                                          quiet || agent_supported)) {
-                               packet_buf_write(&req_buf,
-                                                "%s %s %s%c%s%s%s%s%s",
-                                                old_hex, new_hex, ref->name, 0,
-                                                status_report ? " report-status" : "",
-                                                use_sideband ? " side-band-64k" : "",
-                                                quiet ? " quiet" : "",
-                                                agent_supported ? " agent=" : "",
-                                                agent_supported ? git_user_agent_sanitized() : ""
-                                               );
-                       }
-                       else
-                               packet_buf_write(&req_buf, "%s %s %s",
-                                                old_hex, new_hex, ref->name);
-                       ref->status = status_report ?
-                               REF_STATUS_EXPECTING_REPORT :
-                               REF_STATUS_OK;
-                       cmds_sent++;
+                       packet_buf_write(&req_buf, "%s %s %s",
+                                        old_hex, new_hex, ref->name);
                }
        }
  
                packet_flush(out);
        }
        strbuf_release(&req_buf);
+       strbuf_release(&cap_buf);
  
        if (use_sideband && cmds_sent) {
                memset(&demux, 0, sizeof(demux));
                in = demux.out;
        }
  
-       if (new_refs && cmds_sent) {
+       if (need_pack_data && cmds_sent) {
                if (pack_objects(out, remote_refs, extra_have, args) < 0) {
                        for (ref = remote_refs; ref; ref = ref->next)
                                ref->status = REF_STATUS_NONE;
index db1998873cee18a74d98d13963ad640bc1f8abbe,ffb3af44984ee4b22dcda50f4f3947d96c95e138..d2c681ebfde39fcccef190c3a242dfae9d8af2f2
@@@ -12,6 -12,7 +12,7 @@@ if test -n "$NO_CURL"; the
  fi
  
  ROOT_PATH="$PWD"
+ . "$TEST_DIRECTORY"/lib-gpg.sh
  . "$TEST_DIRECTORY"/lib-httpd.sh
  . "$TEST_DIRECTORY"/lib-terminal.sh
  start_httpd
@@@ -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 --combined t/test-lib.sh
index 82095e34eeb7ce9932abe1a5e97b1c2eb0888033,d5939b70f3e9b3e6bd9d3e08d342f90729bc7268..0f4a67bfc63a989883007911f889ba5aed1c0aac
@@@ -813,7 -813,8 +813,8 @@@ rm -fr "$TRASH_DIRECTORY" || 
  }
  
  HOME="$TRASH_DIRECTORY"
- export HOME
+ GNUPGHOME="$HOME/gnupg-home-not-used"
+ export HOME GNUPGHOME
  
  if test -z "$TEST_NO_CREATE_REPO"
  then
@@@ -870,7 -871,7 +871,7 @@@ case $(uname -s) i
        # backslashes in pathspec are converted to '/'
        # exec does not inherit the PID
        test_set_prereq MINGW
 -      test_set_prereq NOT_CYGWIN
 +      test_set_prereq NATIVE_CRLF
        test_set_prereq SED_STRIPS_CR
        test_set_prereq GREP_STRIPS_CR
        GIT_TEST_CMP=mingw_test_cmp
  *CYGWIN*)
        test_set_prereq POSIXPERM
        test_set_prereq EXECKEEPSPID
 -      test_set_prereq NOT_MINGW
        test_set_prereq CYGWIN
        test_set_prereq SED_STRIPS_CR
        test_set_prereq GREP_STRIPS_CR
        test_set_prereq POSIXPERM
        test_set_prereq BSLASHPSPEC
        test_set_prereq EXECKEEPSPID
 -      test_set_prereq NOT_MINGW
 -      test_set_prereq NOT_CYGWIN
        ;;
  esac
  
diff --combined transport-helper.c
index 080a7a6ae23e6915c425a3e471fd641aabafba90,4b1a26143aad45db5dd68991da692a4cef64e182..2b24d51a24adb9dfc885591ceb1653ace65f198a
@@@ -118,8 -118,7 +118,8 @@@ static struct child_process *get_helper
        if (data->helper)
                return data->helper;
  
 -      helper = xcalloc(1, sizeof(*helper));
 +      helper = xmalloc(sizeof(*helper));
 +      child_process_init(helper);
        helper->in = -1;
        helper->out = -1;
        helper->err = 0;
@@@ -260,7 -259,8 +260,8 @@@ static const char *unsupported_options[
  static const char *boolean_options[] = {
        TRANS_OPT_THIN,
        TRANS_OPT_KEEP,
-       TRANS_OPT_FOLLOWTAGS
+       TRANS_OPT_FOLLOWTAGS,
+       TRANS_OPT_PUSH_CERT
        };
  
  static int set_helper_option(struct transport *transport,
@@@ -396,7 -396,7 +397,7 @@@ static int get_importer(struct transpor
        struct child_process *helper = get_helper(transport);
        struct helper_data *data = transport->data;
        int cat_blob_fd, code;
 -      memset(fastimport, 0, sizeof(*fastimport));
 +      child_process_init(fastimport);
        fastimport->in = helper->out;
        argv_array_push(&fastimport->args, "fast-import");
        argv_array_push(&fastimport->args, debug ? "--stats" : "--quiet");
@@@ -836,6 -836,9 +837,9 @@@ static int push_refs_with_push(struct t
        if (flags & TRANSPORT_PUSH_DRY_RUN) {
                if (set_helper_option(transport, "dry-run", "true") != 0)
                        die("helper %s does not support dry-run", data->name);
+       } else if (flags & TRANSPORT_PUSH_CERT) {
+               if (set_helper_option(transport, TRANS_OPT_PUSH_CERT, "true") != 0)
+                       die("helper %s does not support --signed", data->name);
        }
  
        strbuf_addch(&buf, '\n');
@@@ -860,6 -863,9 +864,9 @@@ static int push_refs_with_export(struc
        if (flags & TRANSPORT_PUSH_DRY_RUN) {
                if (set_helper_option(transport, "dry-run", "true") != 0)
                        die("helper %s does not support dry-run", data->name);
+       } else if (flags & TRANSPORT_PUSH_CERT) {
+               if (set_helper_option(transport, TRANS_OPT_PUSH_CERT, "true") != 0)
+                       die("helper %s does not support dry-run", data->name);
        }
  
        if (flags & TRANSPORT_PUSH_FORCE) {
diff --combined transport.c
index 7388bb87dae96514801bcd604b74d31f54955f9a,1df13753a6be3673efa9d9a5ba1c8a226be669a9..055d2a27d945de69f20a890d5e184b7bd09f93d9
@@@ -201,7 -201,7 +201,7 @@@ static struct ref *get_refs_via_rsync(s
  {
        struct strbuf buf = STRBUF_INIT, temp_dir = STRBUF_INIT;
        struct ref dummy = {NULL}, *tail = &dummy;
 -      struct child_process rsync;
 +      struct child_process rsync = CHILD_PROCESS_INIT;
        const char *args[5];
        int temp_dir_len;
  
        strbuf_addstr(&buf, rsync_url(transport->url));
        strbuf_addstr(&buf, "/refs");
  
 -      memset(&rsync, 0, sizeof(rsync));
        rsync.argv = args;
        rsync.stdout_to_stderr = 1;
        args[0] = "rsync";
  static int fetch_objs_via_rsync(struct transport *transport,
                                int nr_objs, struct ref **to_fetch)
  {
 -      struct child_process rsync;
 +      struct child_process rsync = CHILD_PROCESS_INIT;
  
 -      memset(&rsync, 0, sizeof(rsync));
        rsync.stdout_to_stderr = 1;
        argv_array_push(&rsync.args, "rsync");
        argv_array_push(&rsync.args, (transport->verbose > 1) ? "-rv" : "-r");
@@@ -325,7 -327,7 +325,7 @@@ static int rsync_transport_push(struct 
  {
        struct strbuf buf = STRBUF_INIT, temp_dir = STRBUF_INIT;
        int result = 0, i;
 -      struct child_process rsync;
 +      struct child_process rsync = CHILD_PROCESS_INIT;
        const char *args[10];
  
        if (flags & TRANSPORT_PUSH_MIRROR)
        strbuf_addstr(&buf, rsync_url(transport->url));
        strbuf_addch(&buf, '/');
  
 -      memset(&rsync, 0, sizeof(rsync));
        rsync.argv = args;
        rsync.stdout_to_stderr = 1;
        i = 0;
@@@ -477,6 -480,9 +477,9 @@@ static int set_git_option(struct git_tr
                                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;
  }
@@@ -820,6 -826,8 +823,8 @@@ static int git_transport_push(struct tr
        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);
+       args.url = transport->url;
  
        ret = send_pack(&args, data->fd, data->conn, remote_refs,
                        &data->extra_have);
@@@ -1053,7 -1061,7 +1058,7 @@@ static int run_pre_push_hook(struct tra
  {
        int ret = 0, x;
        struct ref *r;
 -      struct child_process proc;
 +      struct child_process proc = CHILD_PROCESS_INIT;
        struct strbuf buf;
        const char *argv[4];
  
        argv[2] = transport->url;
        argv[3] = NULL;
  
 -      memset(&proc, 0, sizeof(proc));
        proc.argv = argv;
        proc.in = -1;