summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2014-10-08 13:05:15 -0700
committerJunio C Hamano <gitster@pobox.com>2014-10-08 13:05:25 -0700
commitfb06b5280ea05d75515fa780cf08d4ec9d6fe101 (patch)
tree9d7c7032df370076149f31f3373e067b69248169
parent325602ce120e7bd7321b9ed409b49b48fd20888e (diff)
parent6f5ef44e0d8933621fcd50127518557013002313 (diff)
downloadgit-fb06b5280ea05d75515fa780cf08d4ec9d6fe101.tar.gz
Merge branch 'jc/push-cert'
Allow "git push" request to be signed, so that it can be verified and audited, using the GPG signature of the person who pushed, that the tips of branches at a public repository really point the commits the pusher wanted to, without having to "trust" the server. * jc/push-cert: (24 commits) receive-pack::hmac_sha1(): copy the entire SHA-1 hash out signed push: allow stale nonce in stateless mode signed push: teach smart-HTTP to pass "git push --signed" around signed push: fortify against replay attacks signed push: add "pushee" header to push certificate signed push: remove duplicated protocol info send-pack: send feature request on push-cert packet receive-pack: GPG-validate push certificates push: the beginning of "git push --signed" pack-protocol doc: typofix for PKT-LINE gpg-interface: move parse_signature() to where it should be gpg-interface: move parse_gpg_output() to where it should be send-pack: clarify that cmds_sent is a boolean send-pack: refactor inspecting and resetting status and sending commands send-pack: rename "new_refs" to "need_pack_data" receive-pack: factor out capability string generation send-pack: factor out capability string generation send-pack: always send capabilities send-pack: refactor decision to send update per ref send-pack: move REF_STATUS_REJECT_NODELETE logic a bit higher ...
-rw-r--r--Documentation/config.txt19
-rw-r--r--Documentation/git-push.txt9
-rw-r--r--Documentation/git-receive-pack.txt65
-rw-r--r--Documentation/technical/pack-protocol.txt49
-rw-r--r--Documentation/technical/protocol-capabilities.txt13
-rw-r--r--builtin/push.c1
-rw-r--r--builtin/receive-pack.c393
-rw-r--r--builtin/send-pack.c4
-rw-r--r--commit.c36
-rw-r--r--gpg-interface.c57
-rw-r--r--gpg-interface.h17
-rw-r--r--remote-curl.c13
-rw-r--r--send-pack.c201
-rw-r--r--send-pack.h2
-rw-r--r--t/lib-httpd/apache.conf1
-rwxr-xr-xt/t5534-push-signed.sh127
-rwxr-xr-xt/t5541-http-push-smart.sh41
-rw-r--r--t/test-lib.sh3
-rw-r--r--tag.c20
-rw-r--r--tag.h1
-rw-r--r--transport-helper.c9
-rw-r--r--transport.c5
-rw-r--r--transport.h5
23 files changed, 932 insertions, 159 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 3b5b24aeb7..04a1e2f37e 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -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
diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
index c0d7403b9a..b17283ab7a 100644
--- a/Documentation/git-push.txt
+++ b/Documentation/git-push.txt
@@ -10,7 +10,8 @@ SYNOPSIS
--------
[verse]
'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<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
diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt
index b1f7dc643a..9016960e27 100644
--- a/Documentation/git-receive-pack.txt
+++ b/Documentation/git-receive-pack.txt
@@ -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
diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt
index 569c48a352..462e20645f 100644
--- a/Documentation/technical/pack-protocol.txt
+++ b/Documentation/technical/pack-protocol.txt
@@ -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
-------------
diff --git a/Documentation/technical/protocol-capabilities.txt b/Documentation/technical/protocol-capabilities.txt
index e174343847..0c92deebcc 100644
--- a/Documentation/technical/protocol-capabilities.txt
+++ b/Documentation/technical/protocol-capabilities.txt
@@ -18,8 +18,8 @@ was sent. Server MUST NOT ignore capabilities that client requested
and server advertised. As a consequence of these rules, server MUST
NOT advertise capabilities it does not understand.
-The 'report-status', 'delete-refs', and 'quiet' capabilities are sent and
-recognized by the receive-pack (push to server) process.
+The 'report-status', 'delete-refs', 'quiet', and 'push-cert' capabilities
+are sent and recognized by the receive-pack (push to server) process.
The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized
by both upload-pack and receive-pack protocols. The 'agent' capability
@@ -250,3 +250,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.
diff --git a/builtin/push.c b/builtin/push.c
index f50e3d5e77..ae56f73a66 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -506,6 +506,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"),
TRANSPORT_PUSH_FOLLOW_TAGS),
+ OPT_BIT(0, "signed", &flags, N_("GPG sign the push"), TRANSPORT_PUSH_CERT),
OPT_END()
};
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index daf0600ca3..a01ac2096a 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -15,6 +15,8 @@
#include "connected.h"
#include "argv-array.h"
#include "version.h"
+#include "tag.h"
+#include "gpg-interface.h"
#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;
}
diff --git a/builtin/send-pack.c b/builtin/send-pack.c
index 4b1bc0fef7..b564a77845 100644
--- a/builtin/send-pack.c
+++ b/builtin/send-pack.c
@@ -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;
diff --git a/commit.c b/commit.c
index 9c4439fed6..19cf8f9c67 100644
--- 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;
diff --git a/gpg-interface.c b/gpg-interface.c
index 1ef73fb7df..68b0c814f7 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -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);
diff --git a/gpg-interface.h b/gpg-interface.h
index 37c23daff0..87a4f2e3fa 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -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 *);
diff --git a/remote-curl.c b/remote-curl.c
index cd626d15e5..dd63bc27ab 100644
--- a/remote-curl.c
+++ b/remote-curl.c
@@ -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)
diff --git a/send-pack.c b/send-pack.c
index 8b4cbf049c..949cb61aa0 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -11,6 +11,7 @@
#include "transport.h"
#include "version.h"
#include "sha1-array.h"
+#include "gpg-interface.h"
static int feed_object(const unsigned char *sha1, int fd, int negative)
{
@@ -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;
diff --git a/send-pack.h b/send-pack.h
index 8e843924cf..5635457746 100644
--- a/send-pack.h
+++ b/send-pack.h
@@ -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;
};
diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf
index b384d79935..7713dd2609 100644
--- a/t/lib-httpd/apache.conf
+++ b/t/lib-httpd/apache.conf
@@ -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
index 0000000000..2786346f9a
--- /dev/null
+++ b/t/t5534-push-signed.sh
@@ -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
diff --git a/t/t5541-http-push-smart.sh b/t/t5541-http-push-smart.sh
index db1998873c..d2c681ebfd 100755
--- a/t/t5541-http-push-smart.sh
+++ b/t/t5541-http-push-smart.sh
@@ -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
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 82095e34ee..0f4a67bfc6 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -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 82d841bf2d..5b0ac62ed8 100644
--- 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 bc8a1e40f0..f4580aea42 100644
--- 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 */
diff --git a/transport-helper.c b/transport-helper.c
index 080a7a6ae2..2b24d51a24 100644
--- a/transport-helper.c
+++ b/transport-helper.c
@@ -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) {
diff --git a/transport.c b/transport.c
index 7388bb87da..055d2a27d9 100644
--- a/transport.c
+++ b/transport.c
@@ -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);
diff --git a/transport.h b/transport.h
index 02ea248db1..3e0091eaab 100644
--- a/transport.h
+++ b/transport.h
@@ -12,6 +12,7 @@ struct git_transport_options {
unsigned check_self_contained_and_connected : 1;
unsigned self_contained_and_connected : 1;
unsigned update_shallow : 1;
+ unsigned push_cert : 1;
int depth;
const char *uploadpack;
const char *receivepack;
@@ -123,6 +124,7 @@ struct transport {
#define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256
#define TRANSPORT_PUSH_NO_HOOK 512
#define TRANSPORT_PUSH_FOLLOW_TAGS 1024
+#define TRANSPORT_PUSH_CERT 2048
#define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
#define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)
@@ -156,6 +158,9 @@ struct transport *transport_get(struct remote *, const char *);
/* Accept refs that may update .git/shallow without --depth */
#define TRANS_OPT_UPDATE_SHALLOW "updateshallow"
+/* Send push certificates */
+#define TRANS_OPT_PUSH_CERT "pushcert"
+
/**
* Returns 0 if the option was used, non-zero otherwise. Prints a
* message to stderr if the option is not used.