]> git.ipfire.org Git - thirdparty/git.git/commitdiff
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
  ...

23 files changed:
Documentation/config.txt
Documentation/git-push.txt
Documentation/git-receive-pack.txt
Documentation/technical/pack-protocol.txt
Documentation/technical/protocol-capabilities.txt
builtin/push.c
builtin/receive-pack.c
builtin/send-pack.c
commit.c
gpg-interface.c
gpg-interface.h
remote-curl.c
send-pack.c
send-pack.h
t/lib-httpd/apache.conf
t/t5534-push-signed.sh [new file with mode: 0755]
t/t5541-http-push-smart.sh
t/test-lib.sh
tag.c
tag.h
transport-helper.c
transport.c
transport.h

index 3b5b24aeb7f16c03e06bd6f38fd89e9080011ce7..04a1e2f37e938f004c899d3fb6c59d282c2071ce 100644 (file)
@@ -2044,6 +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..b17283ab7a1cc73c5ec741e0da1128bea2a57a65 100644 (file)
@@ -10,7 +10,8 @@ SYNOPSIS
 --------
 [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>...]]
 
@@ -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=<git-receive-pack>::
 --exec=<git-receive-pack>::
        Path to the 'git-receive-pack' program on the remote
index b1f7dc643a0e9b6b232e1c75b39d7f784ab21b4a..9016960e27164a11d687ed16b34245667e271fe3 100644 (file)
@@ -53,6 +53,56 @@ 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.  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]).
+
+`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.
+`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.
 
@@ -101,9 +151,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 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:
+ref listing the commits pushed to the repository, and logs the push
+certificates of signed pushes with good signatures to a logger
+service:
 
        #!/bin/sh
        # mail out commit update information.
@@ -119,6 +174,14 @@ 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-}" && 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
        exit 0
 
 The exit code from this hook invocation is ignored, however a
index 569c48a352b76ab3fc0d31f21f0f1a1bd654f41d..462e20645f1ea87dcc04938b8ca504bd0d7d7636 100644 (file)
@@ -212,9 +212,9 @@ out of what the server said it could do with the first 'want' line.
   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,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 LF)
 
@@ -481,12 +481,27 @@ 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("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 @@ 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 <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
 -------------
 
index e17434384724662aabb60d7fceed40a433207608..0c92deebcca2296c4938e7b2ae1733e35b2ce9c4 100644 (file)
@@ -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,12 @@ 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=<nonce>
+-----------------
+
+The receive-pack server that advertises this capability is willing
+to accept a signed push certificate, and asks the <nonce> 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.
index f50e3d5e77db6842ff3450560aad330f6a4bd91d..ae56f73a66edde6ee8c8b6be349d9e31f29699dc 100644 (file)
@@ -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()
        };
 
index daf0600ca30eb3969e0583cc1096e6f33291d4aa..a01ac2096a70fcfbe4f206cca1b06ded1a1daffc 100644 (file)
@@ -15,6 +15,8 @@
 #include "connected.h"
 #include "argv-array.h"
 #include "version.h"
+#include "tag.h"
+#include "gpg-interface.h"
 #include "sigchain.h"
 
 static const char receive_pack_usage[] = "git receive-pack <git-dir>";
@@ -42,11 +44,27 @@ static int prefer_ofs_delta = 1;
 static int auto_update_server_info;
 static int auto_gc = 1;
 static int fix_thin = 1;
+static int stateless_rpc;
+static const char *service_dir;
 static const char *head_name;
 static void *head_name_to_free;
 static int sent_capabilities;
 static int shallow_update;
 static const char *alt_shallow_file;
+static struct strbuf push_cert = STRBUF_INIT;
+static unsigned char push_cert_sha1[20];
+static struct signature_check sigcheck;
+static const char *push_cert_nonce;
+static const char *cert_nonce_seed;
+
+static const char *NONCE_UNSOLICITED = "UNSOLICITED";
+static const char *NONCE_BAD = "BAD";
+static const char *NONCE_MISSING = "MISSING";
+static const char *NONCE_OK = "OK";
+static const char *NONCE_SLOP = "SLOP";
+static const char *nonce_status;
+static long nonce_stamp_slop;
+static unsigned long nonce_stamp_slop_limit;
 
 static enum deny_action parse_deny_action(const char *var, const char *value)
 {
@@ -130,6 +148,14 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
                return 0;
        }
 
+       if (strcmp(var, "receive.certnonceseed") == 0)
+               return git_config_string(&cert_nonce_seed, var, value);
+
+       if (strcmp(var, "receive.certnonceslop") == 0) {
+               nonce_stamp_slop_limit = git_config_ulong(var, value);
+               return 0;
+       }
+
        return git_default_config(var, value, cb);
 }
 
@@ -138,15 +164,23 @@ static void show_ref(const char *path, const unsigned char *sha1)
        if (ref_is_hidden(path))
                return;
 
-       if (sent_capabilities)
+       if (sent_capabilities) {
                packet_write(1, "%s %s\n", sha1_to_hex(sha1), path);
-       else
-               packet_write(1, "%s %s%c%s%s agent=%s\n",
-                            sha1_to_hex(sha1), path, 0,
-                            " report-status delete-refs side-band-64k quiet",
-                            prefer_ofs_delta ? " ofs-delta" : "",
-                            git_user_agent_sanitized());
-       sent_capabilities = 1;
+       } else {
+               struct strbuf cap = STRBUF_INIT;
+
+               strbuf_addstr(&cap,
+                             "report-status delete-refs side-band-64k quiet");
+               if (prefer_ofs_delta)
+                       strbuf_addstr(&cap, " ofs-delta");
+               if (push_cert_nonce)
+                       strbuf_addf(&cap, " push-cert=%s", push_cert_nonce);
+               strbuf_addf(&cap, " agent=%s", git_user_agent_sanitized());
+               packet_write(1, "%s %s%c%s\n",
+                            sha1_to_hex(sha1), path, 0, cap.buf);
+               strbuf_release(&cap);
+               sent_capabilities = 1;
+       }
 }
 
 static int show_ref_cb(const char *path, const unsigned char *sha1, int flag, void *unused)
@@ -253,6 +287,222 @@ static int copy_to_sideband(int in, int out, void *arg)
        return 0;
 }
 
+#define HMAC_BLOCK_SIZE 64
+
+static void hmac_sha1(unsigned char *out,
+                     const char *key_in, size_t key_len,
+                     const char *text, size_t text_len)
+{
+       unsigned char key[HMAC_BLOCK_SIZE];
+       unsigned char k_ipad[HMAC_BLOCK_SIZE];
+       unsigned char k_opad[HMAC_BLOCK_SIZE];
+       int i;
+       git_SHA_CTX ctx;
+
+       /* RFC 2104 2. (1) */
+       memset(key, '\0', HMAC_BLOCK_SIZE);
+       if (HMAC_BLOCK_SIZE < key_len) {
+               git_SHA1_Init(&ctx);
+               git_SHA1_Update(&ctx, key_in, key_len);
+               git_SHA1_Final(key, &ctx);
+       } else {
+               memcpy(key, key_in, key_len);
+       }
+
+       /* RFC 2104 2. (2) & (5) */
+       for (i = 0; i < sizeof(key); i++) {
+               k_ipad[i] = key[i] ^ 0x36;
+               k_opad[i] = key[i] ^ 0x5c;
+       }
+
+       /* RFC 2104 2. (3) & (4) */
+       git_SHA1_Init(&ctx);
+       git_SHA1_Update(&ctx, k_ipad, sizeof(k_ipad));
+       git_SHA1_Update(&ctx, text, text_len);
+       git_SHA1_Final(out, &ctx);
+
+       /* RFC 2104 2. (6) & (7) */
+       git_SHA1_Init(&ctx);
+       git_SHA1_Update(&ctx, k_opad, sizeof(k_opad));
+       git_SHA1_Update(&ctx, out, 20);
+       git_SHA1_Final(out, &ctx);
+}
+
+static char *prepare_push_cert_nonce(const char *path, unsigned long stamp)
+{
+       struct strbuf buf = STRBUF_INIT;
+       unsigned char sha1[20];
+
+       strbuf_addf(&buf, "%s:%lu", path, stamp);
+       hmac_sha1(sha1, buf.buf, buf.len, cert_nonce_seed, strlen(cert_nonce_seed));;
+       strbuf_release(&buf);
+
+       /* RFC 2104 5. HMAC-SHA1-80 */
+       strbuf_addf(&buf, "%lu-%.*s", stamp, 20, sha1_to_hex(sha1));
+       return strbuf_detach(&buf, NULL);
+}
+
+/*
+ * NEEDSWORK: reuse find_commit_header() from jk/commit-author-parsing
+ * after dropping "_commit" from its name and possibly moving it out
+ * of commit.c
+ */
+static char *find_header(const char *msg, size_t len, const char *key)
+{
+       int key_len = strlen(key);
+       const char *line = msg;
+
+       while (line && line < msg + len) {
+               const char *eol = strchrnul(line, '\n');
+
+               if ((msg + len <= eol) || line == eol)
+                       return NULL;
+               if (line + key_len < eol &&
+                   !memcmp(line, key, key_len) && line[key_len] == ' ') {
+                       int offset = key_len + 1;
+                       return xmemdupz(line + offset, (eol - line) - offset);
+               }
+               line = *eol ? eol + 1 : NULL;
+       }
+       return NULL;
+}
+
+static const char *check_nonce(const char *buf, size_t len)
+{
+       char *nonce = find_header(buf, len, "nonce");
+       unsigned long stamp, ostamp;
+       char *bohmac, *expect = NULL;
+       const char *retval = NONCE_BAD;
+
+       if (!nonce) {
+               retval = NONCE_MISSING;
+               goto leave;
+       } else if (!push_cert_nonce) {
+               retval = NONCE_UNSOLICITED;
+               goto leave;
+       } else if (!strcmp(push_cert_nonce, nonce)) {
+               retval = NONCE_OK;
+               goto leave;
+       }
+
+       if (!stateless_rpc) {
+               /* returned nonce MUST match what we gave out earlier */
+               retval = NONCE_BAD;
+               goto leave;
+       }
+
+       /*
+        * In stateless mode, we may be receiving a nonce issued by
+        * another instance of the server that serving the same
+        * repository, and the timestamps may not match, but the
+        * nonce-seed and dir should match, so we can recompute and
+        * report the time slop.
+        *
+        * In addition, when a nonce issued by another instance has
+        * timestamp within receive.certnonceslop seconds, we pretend
+        * as if we issued that nonce when reporting to the hook.
+        */
+
+       /* nonce is concat(<seconds-since-epoch>, "-", <hmac>) */
+       if (*nonce <= '0' || '9' < *nonce) {
+               retval = NONCE_BAD;
+               goto leave;
+       }
+       stamp = strtoul(nonce, &bohmac, 10);
+       if (bohmac == nonce || bohmac[0] != '-') {
+               retval = NONCE_BAD;
+               goto leave;
+       }
+
+       expect = prepare_push_cert_nonce(service_dir, stamp);
+       if (strcmp(expect, nonce)) {
+               /* Not what we would have signed earlier */
+               retval = NONCE_BAD;
+               goto leave;
+       }
+
+       /*
+        * By how many seconds is this nonce stale?  Negative value
+        * would mean it was issued by another server with its clock
+        * skewed in the future.
+        */
+       ostamp = strtoul(push_cert_nonce, NULL, 10);
+       nonce_stamp_slop = (long)ostamp - (long)stamp;
+
+       if (nonce_stamp_slop_limit &&
+           abs(nonce_stamp_slop) <= nonce_stamp_slop_limit) {
+               /*
+                * Pretend as if the received nonce (which passes the
+                * HMAC check, so it is not a forged by third-party)
+                * is what we issued.
+                */
+               free((void *)push_cert_nonce);
+               push_cert_nonce = xstrdup(nonce);
+               retval = NONCE_OK;
+       } else {
+               retval = NONCE_SLOP;
+       }
+
+leave:
+       free(nonce);
+       free(expect);
+       return retval;
+}
+
+static void prepare_push_cert_sha1(struct child_process *proc)
+{
+       static int already_done;
+       struct argv_array env = ARGV_ARRAY_INIT;
+
+       if (!push_cert.len)
+               return;
+
+       if (!already_done) {
+               struct strbuf gpg_output = STRBUF_INIT;
+               struct strbuf gpg_status = STRBUF_INIT;
+               int bogs /* beginning_of_gpg_sig */;
+
+               already_done = 1;
+               if (write_sha1_file(push_cert.buf, push_cert.len, "blob", push_cert_sha1))
+                       hashclr(push_cert_sha1);
+
+               memset(&sigcheck, '\0', sizeof(sigcheck));
+               sigcheck.result = 'N';
+
+               bogs = parse_signature(push_cert.buf, push_cert.len);
+               if (verify_signed_buffer(push_cert.buf, bogs,
+                                        push_cert.buf + bogs, push_cert.len - bogs,
+                                        &gpg_output, &gpg_status) < 0) {
+                       ; /* error running gpg */
+               } else {
+                       sigcheck.payload = push_cert.buf;
+                       sigcheck.gpg_output = gpg_output.buf;
+                       sigcheck.gpg_status = gpg_status.buf;
+                       parse_gpg_output(&sigcheck);
+               }
+
+               strbuf_release(&gpg_output);
+               strbuf_release(&gpg_status);
+               nonce_status = check_nonce(push_cert.buf, bogs);
+       }
+       if (!is_null_sha1(push_cert_sha1)) {
+               argv_array_pushf(&env, "GIT_PUSH_CERT=%s", sha1_to_hex(push_cert_sha1));
+               argv_array_pushf(&env, "GIT_PUSH_CERT_SIGNER=%s",
+                                sigcheck.signer ? sigcheck.signer : "");
+               argv_array_pushf(&env, "GIT_PUSH_CERT_KEY=%s",
+                                sigcheck.key ? sigcheck.key : "");
+               argv_array_pushf(&env, "GIT_PUSH_CERT_STATUS=%c", sigcheck.result);
+               if (push_cert_nonce) {
+                       argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE=%s", push_cert_nonce);
+                       argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_STATUS=%s", nonce_status);
+                       if (nonce_status == NONCE_SLOP)
+                               argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_SLOP=%ld",
+                                                nonce_stamp_slop);
+               }
+               proc->env = env.argv;
+       }
+}
+
 typedef int (*feed_fn)(void *, const char **, size_t *);
 static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_state)
 {
@@ -271,6 +521,8 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta
        proc.in = -1;
        proc.stdout_to_stderr = 1;
 
+       prepare_push_cert_sha1(&proc);
+
        if (use_sideband) {
                memset(&muxer, 0, sizeof(muxer));
                muxer.proc = copy_to_sideband;
@@ -841,40 +1093,79 @@ static void execute_commands(struct command *commands,
                      "the reported refs above");
 }
 
+static struct command **queue_command(struct command **tail,
+                                     const char *line,
+                                     int linelen)
+{
+       unsigned char old_sha1[20], new_sha1[20];
+       struct command *cmd;
+       const char *refname;
+       int reflen;
+
+       if (linelen < 83 ||
+           line[40] != ' ' ||
+           line[81] != ' ' ||
+           get_sha1_hex(line, old_sha1) ||
+           get_sha1_hex(line + 41, new_sha1))
+               die("protocol error: expected old/new/ref, got '%s'", line);
+
+       refname = line + 82;
+       reflen = linelen - 82;
+       cmd = xcalloc(1, sizeof(struct command) + reflen + 1);
+       hashcpy(cmd->old_sha1, old_sha1);
+       hashcpy(cmd->new_sha1, new_sha1);
+       memcpy(cmd->ref_name, refname, reflen);
+       cmd->ref_name[reflen] = '\0';
+       *tail = cmd;
+       return &cmd->next;
+}
+
+static void queue_commands_from_cert(struct command **tail,
+                                    struct strbuf *push_cert)
+{
+       const char *boc, *eoc;
+
+       if (*tail)
+               die("protocol error: got both push certificate and unsigned commands");
+
+       boc = strstr(push_cert->buf, "\n\n");
+       if (!boc)
+               die("malformed push certificate %.*s", 100, push_cert->buf);
+       else
+               boc += 2;
+       eoc = push_cert->buf + parse_signature(push_cert->buf, push_cert->len);
+
+       while (boc < eoc) {
+               const char *eol = memchr(boc, '\n', eoc - boc);
+               tail = queue_command(tail, boc, eol ? eol - boc : eoc - eol);
+               boc = eol ? eol + 1 : eoc;
+       }
+}
+
 static struct command *read_head_info(struct sha1_array *shallow)
 {
        struct command *commands = NULL;
        struct command **p = &commands;
        for (;;) {
                char *line;
-               unsigned char old_sha1[20], new_sha1[20];
-               struct command *cmd;
-               char *refname;
-               int len, reflen;
+               int len, linelen;
 
                line = packet_read_line(0, &len);
                if (!line)
                        break;
 
                if (len == 48 && starts_with(line, "shallow ")) {
-                       if (get_sha1_hex(line + 8, old_sha1))
-                               die("protocol error: expected shallow sha, got '%s'", line + 8);
-                       sha1_array_append(shallow, old_sha1);
+                       unsigned char sha1[20];
+                       if (get_sha1_hex(line + 8, sha1))
+                               die("protocol error: expected shallow sha, got '%s'",
+                                   line + 8);
+                       sha1_array_append(shallow, sha1);
                        continue;
                }
 
-               if (len < 83 ||
-                   line[40] != ' ' ||
-                   line[81] != ' ' ||
-                   get_sha1_hex(line, old_sha1) ||
-                   get_sha1_hex(line + 41, new_sha1))
-                       die("protocol error: expected old/new/ref, got '%s'",
-                           line);
-
-               refname = line + 82;
-               reflen = strlen(refname);
-               if (reflen + 82 < len) {
-                       const char *feature_list = refname + reflen + 1;
+               linelen = strlen(line);
+               if (linelen < len) {
+                       const char *feature_list = line + linelen + 1;
                        if (parse_feature_request(feature_list, "report-status"))
                                report_status = 1;
                        if (parse_feature_request(feature_list, "side-band-64k"))
@@ -882,13 +1173,34 @@ static struct command *read_head_info(struct sha1_array *shallow)
                        if (parse_feature_request(feature_list, "quiet"))
                                quiet = 1;
                }
-               cmd = xcalloc(1, sizeof(struct command) + len - 80);
-               hashcpy(cmd->old_sha1, old_sha1);
-               hashcpy(cmd->new_sha1, new_sha1);
-               memcpy(cmd->ref_name, line + 82, len - 81);
-               *p = cmd;
-               p = &cmd->next;
+
+               if (!strcmp(line, "push-cert")) {
+                       int true_flush = 0;
+                       char certbuf[1024];
+
+                       for (;;) {
+                               len = packet_read(0, NULL, NULL,
+                                                 certbuf, sizeof(certbuf), 0);
+                               if (!len) {
+                                       true_flush = 1;
+                                       break;
+                               }
+                               if (!strcmp(certbuf, "push-cert-end\n"))
+                                       break; /* end of cert */
+                               strbuf_addstr(&push_cert, certbuf);
+                       }
+
+                       if (true_flush)
+                               break;
+                       continue;
+               }
+
+               p = queue_command(p, line, linelen);
        }
+
+       if (push_cert.len)
+               queue_commands_from_cert(p, &push_cert);
+
        return commands;
 }
 
@@ -1129,9 +1441,7 @@ static int delete_only(struct command *commands)
 int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 {
        int advertise_refs = 0;
-       int stateless_rpc = 0;
        int i;
-       const char *dir = NULL;
        struct command *commands;
        struct sha1_array shallow = SHA1_ARRAY_INIT;
        struct sha1_array ref = SHA1_ARRAY_INIT;
@@ -1164,19 +1474,21 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
 
                        usage(receive_pack_usage);
                }
-               if (dir)
+               if (service_dir)
                        usage(receive_pack_usage);
-               dir = arg;
+               service_dir = arg;
        }
-       if (!dir)
+       if (!service_dir)
                usage(receive_pack_usage);
 
        setup_path();
 
-       if (!enter_repo(dir, 0))
-               die("'%s' does not appear to be a git repository", dir);
+       if (!enter_repo(service_dir, 0))
+               die("'%s' does not appear to be a git repository", service_dir);
 
        git_config(receive_pack_config, NULL);
+       if (cert_nonce_seed)
+               push_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL));
 
        if (0 <= transfer_unpack_limit)
                unpack_limit = transfer_unpack_limit;
@@ -1221,5 +1533,6 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
                packet_flush(1);
        sha1_array_clear(&shallow);
        sha1_array_clear(&ref);
+       free((void *)push_cert_nonce);
        return 0;
 }
index 4b1bc0fef72c9764a4e4a43bb30f2c820d1aec34..b564a778455c352379127c3f06349e0ebbc81c98 100644 (file)
@@ -154,6 +154,10 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
                                args.verbose = 1;
                                continue;
                        }
+                       if (!strcmp(arg, "--signed")) {
+                               args.push_cert = 1;
+                               continue;
+                       }
                        if (!strcmp(arg, "--progress")) {
                                progress = 1;
                                continue;
index 9c4439fed61c3b26d493d0f0306b65a471da9b19..19cf8f9c67bd0f4d71172f33424f7d2371a3af68 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -1214,42 +1214,6 @@ 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)
 {
        struct strbuf payload = STRBUF_INIT;
index 1ef73fb7dfedd8cd5d3444433218ff177a63111b..68b0c814f789f39151d380b1d318d6f80090d524 100644 (file)
@@ -7,6 +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);
@@ -21,6 +24,60 @@ void signature_check_clear(struct signature_check *sigc)
        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);
index 37c23daff010b0de18fa12ff6a6167f45ff41ffc..87a4f2e3fad92b4622a40ef5612db293bf4c5dd4 100644 (file)
@@ -5,16 +5,23 @@ struct signature_check {
        char *payload;
        char *gpg_output;
        char *gpg_status;
-       char result; /* 0 (not checked),
-                     * N (checked but no further result),
-                     * U (untrusted good),
-                     * G (good)
-                     * B (bad) */
+
+       /*
+        * possible "result":
+        * 0 (not checked)
+        * N (checked but no further result)
+        * U (untrusted good)
+        * G (good)
+        * B (bad)
+        */
+       char result;
        char *signer;
        char *key;
 };
 
 extern void signature_check_clear(struct signature_check *sigc);
+extern size_t parse_signature(const char *buf, unsigned long size);
+extern void parse_gpg_output(struct signature_check *);
 extern int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *signing_key);
 extern int verify_signed_buffer(const char *payload, size_t payload_size, const char *signature, size_t signature_size, struct strbuf *gpg_output, struct strbuf *gpg_status);
 extern int git_gpg_config(const char *, const char *, void *);
index cd626d15e59832e6fee74b2ca845a23a5fce77ee..dd63bc27abf8ae4b9e480ba1bb88942db11a0e48 100644 (file)
@@ -25,7 +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 @@ static int set_option(const char *name, const char *value)
                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 */;
        }
@@ -872,6 +881,8 @@ static int push_git(struct discovery *heads, int nr_spec, char **specs)
                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)
index 8b4cbf049c243b8cdc1add94ddf1bf50bcbd8df9..949cb61aa0d681e30b7e8199ef9e31ff0caee089 100644 (file)
@@ -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)
 {
@@ -189,6 +190,94 @@ static void advertise_shallow_grafts_buf(struct strbuf *sb)
        for_each_commit_graft(advertise_shallow_grafts_cb, sb);
 }
 
+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;
+       char *signing_key = xstrdup(get_signing_key());
+       const char *cp, *np;
+       struct strbuf cert = STRBUF_INIT;
+       int update_seen = 0;
+
+       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,
@@ -197,8 +286,9 @@ int send_pack(struct send_pack_args *args,
        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;
@@ -207,6 +297,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"))
@@ -223,6 +314,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) {
+               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"
@@ -230,64 +329,71 @@ int send_pack(struct send_pack_args *args,
                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);
                }
        }
 
@@ -301,6 +407,7 @@ int send_pack(struct send_pack_args *args,
                packet_flush(out);
        }
        strbuf_release(&req_buf);
+       strbuf_release(&cap_buf);
 
        if (use_sideband && cmds_sent) {
                memset(&demux, 0, sizeof(demux));
@@ -312,7 +419,7 @@ int send_pack(struct send_pack_args *args,
                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 8e843924cf5beba6ee680149910cd357cffc73e3..56354577467acfe1bf98652f6bfad9ad7e5db851 100644 (file)
@@ -2,6 +2,7 @@
 #define SEND_PACK_H
 
 struct send_pack_args {
+       const char *url;
        unsigned verbose:1,
                quiet:1,
                porcelain:1,
@@ -11,6 +12,7 @@ struct send_pack_args {
                use_thin_pack:1,
                use_ofs_delta:1,
                dry_run:1,
+               push_cert:1,
                stateless_rpc:1;
 };
 
index b384d7993545365c17f3c8bded756b4e74faf794..7713dd260948c0fca0c0633b6c2cc152f7bc1361 100644 (file)
@@ -68,6 +68,7 @@ LockFile accept.lock
 
 PassEnv GIT_VALGRIND
 PassEnv GIT_VALGRIND_OPTIONS
+PassEnv GNUPGHOME
 
 Alias /dumb/ www/
 Alias /auth/dumb/ www/auth/dumb/
diff --git a/t/t5534-push-signed.sh b/t/t5534-push-signed.sh
new file mode 100755 (executable)
index 0000000..2786346
--- /dev/null
@@ -0,0 +1,127 @@
+#!/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 &&
+       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 &&
+       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 'no certificate for a signed push with no update' '
+       prepare_dst &&
+       mkdir -p dst/.git/hooks &&
+       write_script dst/.git/hooks/post-receive <<-\EOF &&
+       if test -n "${GIT_PUSH_CERT-}"
+       then
+               git cat-file blob $GIT_PUSH_CERT >../push-cert
+       fi
+       EOF
+       git push dst noop &&
+       ! test -f dst/push-cert
+'
+
+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
+       # 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 push --signed dst noop ff +noff &&
+
+       (
+               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" 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
+'
+
+test_done
index db1998873cee18a74d98d13963ad640bc1f8abbe..d2c681ebfde39fcccef190c3a242dfae9d8af2f2 100755 (executable)
@@ -12,6 +12,7 @@ if test -n "$NO_CURL"; then
 fi
 
 ROOT_PATH="$PWD"
+. "$TEST_DIRECTORY"/lib-gpg.sh
 . "$TEST_DIRECTORY"/lib-httpd.sh
 . "$TEST_DIRECTORY"/lib-terminal.sh
 start_httpd
@@ -338,5 +339,45 @@ test_expect_success CMDLINE_LIMIT 'push 2000 tags over http' '
        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
index 82095e34eeb7ce9932abe1a5e97b1c2eb0888033..0f4a67bfc63a989883007911f889ba5aed1c0aac 100644 (file)
@@ -813,7 +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
diff --git a/tag.c b/tag.c
index 82d841bf2df0990f124ca9020673352124f20561..5b0ac62ed846188ad44ceac41d278ff37ec8bd9d 100644 (file)
--- a/tag.c
+++ b/tag.c
@@ -4,9 +4,6 @@
 #include "tree.h"
 #include "blob.h"
 
-#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
-#define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----"
-
 const char *tag_type = "tag";
 
 struct object *deref_tag(struct object *o, const char *warn, int warnlen)
@@ -143,20 +140,3 @@ int parse_tag(struct tag *item)
        free(data);
        return ret;
 }
-
-/*
- * Look at a signed tag object, and 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;
-}
diff --git a/tag.h b/tag.h
index bc8a1e40f04e87a6d502ab9d96022f734c57f4eb..f4580aea42633d4e6d849735cf1eedcbc4730ed2 100644 (file)
--- a/tag.h
+++ b/tag.h
@@ -17,6 +17,5 @@ extern int parse_tag_buffer(struct tag *item, const void *data, unsigned long si
 extern int parse_tag(struct tag *item);
 extern struct object *deref_tag(struct object *, const char *, int);
 extern struct object *deref_tag_noverify(struct object *);
-extern size_t parse_signature(const char *buf, unsigned long size);
 
 #endif /* TAG_H */
index 080a7a6ae23e6915c425a3e471fd641aabafba90..2b24d51a24adb9dfc885591ceb1653ace65f198a 100644 (file)
@@ -260,7 +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,
@@ -836,6 +837,9 @@ static int push_refs_with_push(struct transport *transport,
        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 +864,9 @@ static int push_refs_with_export(struct transport *transport,
        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) {
index 7388bb87dae96514801bcd604b74d31f54955f9a..055d2a27d945de69f20a890d5e184b7bd09f93d9 100644 (file)
@@ -477,6 +477,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;
 }
@@ -820,6 +823,8 @@ 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);
+       args.url = transport->url;
 
        ret = send_pack(&args, data->fd, data->conn, remote_refs,
                        &data->extra_have);
index 02ea248db18a558674825384a5d2678a45483873..3e0091eaabe406759005bc24ce2ff39144caa72d 100644 (file)
@@ -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.