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
  ...
This commit is contained in:
Junio C Hamano 2014-10-08 13:05:15 -07:00
commit fb06b5280e
23 changed files with 933 additions and 160 deletions

View File

@ -2044,6 +2044,25 @@ receive.autogc::
receiving data from git-push and updating refs. You can stop receiving data from git-push and updating refs. You can stop
it by setting this variable to false. 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:: receive.fsckObjects::
If it is set to true, git-receive-pack will check all received 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 objects. It will abort in the case of a malformed object or a

View File

@ -10,7 +10,8 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<git-receive-pack>] '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>]]] [--force-with-lease[=<refname>[:<expect>]]]
[--no-verify] [<repository> [<refspec>...]] [--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 from the remote but are pointing at commit-ish that are
reachable from the refs being pushed. 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>:: --receive-pack=<git-receive-pack>::
--exec=<git-receive-pack>:: --exec=<git-receive-pack>::
Path to the 'git-receive-pack' program on the remote Path to the 'git-receive-pack' program on the remote

View 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 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. 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 This hook is called before any refname is updated and before any
fast-forward checks are performed. 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 0\{40}, otherwise sha1-old and sha1-new should be valid objects in
the repository. 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 Using this hook, it is easy to generate mails describing the updates
to the repository. This example script sends one mail message per 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 #!/bin/sh
# mail out commit update information. # mail out commit update information.
@ -119,6 +174,14 @@ ref listing the commits pushed to the repository:
fi | fi |
mail -s "Changes to ref $ref" commit-list@mydomain mail -s "Changes to ref $ref" commit-list@mydomain
done 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 exit 0
The exit code from this hook invocation is ignored, however a The exit code from this hook invocation is ignored, however a

View File

@ -212,9 +212,9 @@ out of what the server said it could do with the first 'want' line.
want-list = first-want want-list = first-want
*additional-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) first-want = PKT-LINE("want" SP obj-id SP capability-list LF)
additional-want = PKT-LINE("want" SP obj-id 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. 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) shallow = PKT-LINE("shallow" SP obj-id LF)
@ -481,12 +481,27 @@ references.
old-id = obj-id old-id = obj-id
new-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) pack-file = "PACK" 28*(OCTET)
---- ----
If the receiving end does not support delete-refs, the sending end MUST If the receiving end does not support delete-refs, the sending end MUST
NOT ask for delete command. 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'. 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, 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. 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. 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 Report Status
------------- -------------

View 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 and server advertised. As a consequence of these rules, server MUST
NOT advertise capabilities it does not understand. NOT advertise capabilities it does not understand.
The 'report-status', 'delete-refs', and 'quiet' capabilities are sent and The 'report-status', 'delete-refs', 'quiet', and 'push-cert' capabilities
recognized by the receive-pack (push to server) process. are sent and recognized by the receive-pack (push to server) process.
The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized
by both upload-pack and receive-pack protocols. The 'agent' capability 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 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 send "want" lines with SHA-1s that exist at the server but are not
advertised by upload-pack. 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.

View 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, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK),
OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"), OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"),
TRANSPORT_PUSH_FOLLOW_TAGS), TRANSPORT_PUSH_FOLLOW_TAGS),
OPT_BIT(0, "signed", &flags, N_("GPG sign the push"), TRANSPORT_PUSH_CERT),
OPT_END() OPT_END()
}; };

View File

@ -15,6 +15,8 @@
#include "connected.h" #include "connected.h"
#include "argv-array.h" #include "argv-array.h"
#include "version.h" #include "version.h"
#include "tag.h"
#include "gpg-interface.h"
#include "sigchain.h" #include "sigchain.h"
static const char receive_pack_usage[] = "git receive-pack <git-dir>"; 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_update_server_info;
static int auto_gc = 1; static int auto_gc = 1;
static int fix_thin = 1; static int fix_thin = 1;
static int stateless_rpc;
static const char *service_dir;
static const char *head_name; static const char *head_name;
static void *head_name_to_free; static void *head_name_to_free;
static int sent_capabilities; static int sent_capabilities;
static int shallow_update; static int shallow_update;
static const char *alt_shallow_file; 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) 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; 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); 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)) if (ref_is_hidden(path))
return; return;
if (sent_capabilities) if (sent_capabilities) {
packet_write(1, "%s %s\n", sha1_to_hex(sha1), path); packet_write(1, "%s %s\n", sha1_to_hex(sha1), path);
else } else {
packet_write(1, "%s %s%c%s%s agent=%s\n", struct strbuf cap = STRBUF_INIT;
sha1_to_hex(sha1), path, 0,
" report-status delete-refs side-band-64k quiet", strbuf_addstr(&cap,
prefer_ofs_delta ? " ofs-delta" : "", "report-status delete-refs side-band-64k quiet");
git_user_agent_sanitized()); if (prefer_ofs_delta)
sent_capabilities = 1; 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) 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; 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 *); 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) 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.in = -1;
proc.stdout_to_stderr = 1; proc.stdout_to_stderr = 1;
prepare_push_cert_sha1(&proc);
if (use_sideband) { if (use_sideband) {
memset(&muxer, 0, sizeof(muxer)); memset(&muxer, 0, sizeof(muxer));
muxer.proc = copy_to_sideband; muxer.proc = copy_to_sideband;
@ -841,40 +1093,79 @@ static void execute_commands(struct command *commands,
"the reported refs above"); "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) static struct command *read_head_info(struct sha1_array *shallow)
{ {
struct command *commands = NULL; struct command *commands = NULL;
struct command **p = &commands; struct command **p = &commands;
for (;;) { for (;;) {
char *line; char *line;
unsigned char old_sha1[20], new_sha1[20]; int len, linelen;
struct command *cmd;
char *refname;
int len, reflen;
line = packet_read_line(0, &len); line = packet_read_line(0, &len);
if (!line) if (!line)
break; break;
if (len == 48 && starts_with(line, "shallow ")) { if (len == 48 && starts_with(line, "shallow ")) {
if (get_sha1_hex(line + 8, old_sha1)) unsigned char sha1[20];
die("protocol error: expected shallow sha, got '%s'", line + 8); if (get_sha1_hex(line + 8, sha1))
sha1_array_append(shallow, old_sha1); die("protocol error: expected shallow sha, got '%s'",
line + 8);
sha1_array_append(shallow, sha1);
continue; continue;
} }
if (len < 83 || linelen = strlen(line);
line[40] != ' ' || if (linelen < len) {
line[81] != ' ' || const char *feature_list = line + linelen + 1;
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;
if (parse_feature_request(feature_list, "report-status")) if (parse_feature_request(feature_list, "report-status"))
report_status = 1; report_status = 1;
if (parse_feature_request(feature_list, "side-band-64k")) 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")) if (parse_feature_request(feature_list, "quiet"))
quiet = 1; quiet = 1;
} }
cmd = xcalloc(1, sizeof(struct command) + len - 80);
hashcpy(cmd->old_sha1, old_sha1); if (!strcmp(line, "push-cert")) {
hashcpy(cmd->new_sha1, new_sha1); int true_flush = 0;
memcpy(cmd->ref_name, line + 82, len - 81); char certbuf[1024];
*p = cmd;
p = &cmd->next; 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; 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 cmd_receive_pack(int argc, const char **argv, const char *prefix)
{ {
int advertise_refs = 0; int advertise_refs = 0;
int stateless_rpc = 0;
int i; int i;
const char *dir = NULL;
struct command *commands; struct command *commands;
struct sha1_array shallow = SHA1_ARRAY_INIT; struct sha1_array shallow = SHA1_ARRAY_INIT;
struct sha1_array ref = 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); usage(receive_pack_usage);
} }
if (dir) if (service_dir)
usage(receive_pack_usage); usage(receive_pack_usage);
dir = arg; service_dir = arg;
} }
if (!dir) if (!service_dir)
usage(receive_pack_usage); usage(receive_pack_usage);
setup_path(); setup_path();
if (!enter_repo(dir, 0)) if (!enter_repo(service_dir, 0))
die("'%s' does not appear to be a git repository", dir); die("'%s' does not appear to be a git repository", service_dir);
git_config(receive_pack_config, NULL); 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) if (0 <= transfer_unpack_limit)
unpack_limit = 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); packet_flush(1);
sha1_array_clear(&shallow); sha1_array_clear(&shallow);
sha1_array_clear(&ref); sha1_array_clear(&ref);
free((void *)push_cert_nonce);
return 0; return 0;
} }

View File

@ -154,6 +154,10 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
args.verbose = 1; args.verbose = 1;
continue; continue;
} }
if (!strcmp(arg, "--signed")) {
args.push_cert = 1;
continue;
}
if (!strcmp(arg, "--progress")) { if (!strcmp(arg, "--progress")) {
progress = 1; progress = 1;
continue; continue;

View File

@ -1214,42 +1214,6 @@ free_return:
free(buf); free(buf);
} }
static struct {
char result;
const char *check;
} sigcheck_gpg_status[] = {
{ 'G', "\n[GNUPG:] GOODSIG " },
{ 'B', "\n[GNUPG:] BADSIG " },
{ 'U', "\n[GNUPG:] TRUST_NEVER" },
{ 'U', "\n[GNUPG:] TRUST_UNDEFINED" },
};
static void parse_gpg_output(struct signature_check *sigc)
{
const char *buf = sigc->gpg_status;
int i;
/* Iterate over all search strings */
for (i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {
const char *found, *next;
if (!skip_prefix(buf, sigcheck_gpg_status[i].check + 1, &found)) {
found = strstr(buf, sigcheck_gpg_status[i].check);
if (!found)
continue;
found += strlen(sigcheck_gpg_status[i].check);
}
sigc->result = sigcheck_gpg_status[i].result;
/* The trust messages are not followed by key/signer information */
if (sigc->result != 'U') {
sigc->key = xmemdupz(found, 16);
found += 17;
next = strchrnul(found, '\n');
sigc->signer = xmemdupz(found, next - found);
}
}
}
void check_commit_signature(const struct commit *commit, struct signature_check *sigc) void check_commit_signature(const struct commit *commit, struct signature_check *sigc)
{ {
struct strbuf payload = STRBUF_INIT; struct strbuf payload = STRBUF_INIT;

View File

@ -7,6 +7,9 @@
static char *configured_signing_key; static char *configured_signing_key;
static const char *gpg_program = "gpg"; 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) void signature_check_clear(struct signature_check *sigc)
{ {
free(sigc->payload); free(sigc->payload);
@ -21,6 +24,60 @@ void signature_check_clear(struct signature_check *sigc)
sigc->key = NULL; 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) void set_signing_key(const char *key)
{ {
free(configured_signing_key); free(configured_signing_key);

View File

@ -5,16 +5,23 @@ struct signature_check {
char *payload; char *payload;
char *gpg_output; char *gpg_output;
char *gpg_status; char *gpg_status;
char result; /* 0 (not checked),
* N (checked but no further result), /*
* U (untrusted good), * possible "result":
* G (good) * 0 (not checked)
* B (bad) */ * N (checked but no further result)
* U (untrusted good)
* G (good)
* B (bad)
*/
char result;
char *signer; char *signer;
char *key; char *key;
}; };
extern void signature_check_clear(struct signature_check *sigc); 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 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 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 *); extern int git_gpg_config(const char *, const char *, void *);

View File

@ -25,7 +25,8 @@ struct options {
update_shallow : 1, update_shallow : 1,
followtags : 1, followtags : 1,
dry_run : 1, dry_run : 1,
thin : 1; thin : 1,
push_cert : 1;
}; };
static struct options options; static struct options options;
static struct string_list cas_options = STRING_LIST_INIT_DUP; 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 else
return -1; return -1;
return 0; 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 { } else {
return 1 /* unsupported */; return 1 /* unsupported */;
} }
@ -872,6 +881,8 @@ static int push_git(struct discovery *heads, int nr_spec, char **specs)
argv_array_push(&args, "--thin"); argv_array_push(&args, "--thin");
if (options.dry_run) if (options.dry_run)
argv_array_push(&args, "--dry-run"); argv_array_push(&args, "--dry-run");
if (options.push_cert)
argv_array_push(&args, "--signed");
if (options.verbosity == 0) if (options.verbosity == 0)
argv_array_push(&args, "--quiet"); argv_array_push(&args, "--quiet");
else if (options.verbosity > 1) else if (options.verbosity > 1)

View File

@ -11,6 +11,7 @@
#include "transport.h" #include "transport.h"
#include "version.h" #include "version.h"
#include "sha1-array.h" #include "sha1-array.h"
#include "gpg-interface.h"
static int feed_object(const unsigned char *sha1, int fd, int negative) 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); 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 send_pack(struct send_pack_args *args,
int fd[], struct child_process *conn, int fd[], struct child_process *conn,
struct ref *remote_refs, struct ref *remote_refs,
@ -197,8 +286,9 @@ int send_pack(struct send_pack_args *args,
int in = fd[0]; int in = fd[0];
int out = fd[1]; int out = fd[1];
struct strbuf req_buf = STRBUF_INIT; struct strbuf req_buf = STRBUF_INIT;
struct strbuf cap_buf = STRBUF_INIT;
struct ref *ref; struct ref *ref;
int new_refs; int need_pack_data = 0;
int allow_deleting_refs = 0; int allow_deleting_refs = 0;
int status_report = 0; int status_report = 0;
int use_sideband = 0; int use_sideband = 0;
@ -207,6 +297,7 @@ int send_pack(struct send_pack_args *args,
unsigned cmds_sent = 0; unsigned cmds_sent = 0;
int ret; int ret;
struct async demux; struct async demux;
const char *push_cert_nonce = NULL;
/* Does the other end support the reporting? */ /* Does the other end support the reporting? */
if (server_supports("report-status")) if (server_supports("report-status"))
@ -223,6 +314,14 @@ int send_pack(struct send_pack_args *args,
agent_supported = 1; agent_supported = 1;
if (server_supports("no-thin")) if (server_supports("no-thin"))
args->use_thin_pack = 0; 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) { if (!remote_refs) {
fprintf(stderr, "No refs in common and none specified; doing nothing.\n" 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; 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) if (!args->dry_run)
advertise_shallow_grafts_buf(&req_buf); 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);
/*
* Clear the status for each ref and see if we need to send
* the pack data.
*/
for (ref = remote_refs; ref; ref = ref->next) {
if (!ref_update_to_be_sent(ref, args))
continue;
if (!ref->deletion)
need_pack_data = 1;
if (args->dry_run || !status_report)
ref->status = REF_STATUS_OK;
else
ref->status = REF_STATUS_EXPECTING_REPORT;
}
/* /*
* Finally, tell the other end! * Finally, tell the other end!
*/ */
new_refs = 0;
for (ref = remote_refs; ref; ref = ref->next) { for (ref = remote_refs; ref; ref = ref->next) {
if (!ref->peer_ref && !args->send_mirror) char *old_hex, *new_hex;
if (args->dry_run || args->push_cert)
continue; continue;
/* Check for statuses set by set_ref_status_for_push() */ if (!ref_update_to_be_sent(ref, args))
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; continue;
default:
; /* do nothing */
}
if (ref->deletion && !allow_deleting_refs) { old_hex = sha1_to_hex(ref->old_sha1);
ref->status = REF_STATUS_REJECT_NODELETE; new_hex = sha1_to_hex(ref->new_sha1);
continue; if (!cmds_sent) {
} packet_buf_write(&req_buf,
"%s %s %s%c%s",
if (!ref->deletion) old_hex, new_hex, ref->name, 0,
new_refs++; cap_buf.buf);
cmds_sent = 1;
if (args->dry_run) {
ref->status = REF_STATUS_OK;
} else { } else {
char *old_hex = sha1_to_hex(ref->old_sha1); packet_buf_write(&req_buf, "%s %s %s",
char *new_hex = sha1_to_hex(ref->new_sha1); old_hex, new_hex, ref->name);
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++;
} }
} }
@ -301,6 +407,7 @@ int send_pack(struct send_pack_args *args,
packet_flush(out); packet_flush(out);
} }
strbuf_release(&req_buf); strbuf_release(&req_buf);
strbuf_release(&cap_buf);
if (use_sideband && cmds_sent) { if (use_sideband && cmds_sent) {
memset(&demux, 0, sizeof(demux)); memset(&demux, 0, sizeof(demux));
@ -312,7 +419,7 @@ int send_pack(struct send_pack_args *args,
in = demux.out; in = demux.out;
} }
if (new_refs && cmds_sent) { if (need_pack_data && cmds_sent) {
if (pack_objects(out, remote_refs, extra_have, args) < 0) { if (pack_objects(out, remote_refs, extra_have, args) < 0) {
for (ref = remote_refs; ref; ref = ref->next) for (ref = remote_refs; ref; ref = ref->next)
ref->status = REF_STATUS_NONE; ref->status = REF_STATUS_NONE;

View File

@ -2,6 +2,7 @@
#define SEND_PACK_H #define SEND_PACK_H
struct send_pack_args { struct send_pack_args {
const char *url;
unsigned verbose:1, unsigned verbose:1,
quiet:1, quiet:1,
porcelain:1, porcelain:1,
@ -11,6 +12,7 @@ struct send_pack_args {
use_thin_pack:1, use_thin_pack:1,
use_ofs_delta:1, use_ofs_delta:1,
dry_run:1, dry_run:1,
push_cert:1,
stateless_rpc:1; stateless_rpc:1;
}; };

View File

@ -68,6 +68,7 @@ LockFile accept.lock
PassEnv GIT_VALGRIND PassEnv GIT_VALGRIND
PassEnv GIT_VALGRIND_OPTIONS PassEnv GIT_VALGRIND_OPTIONS
PassEnv GNUPGHOME
Alias /dumb/ www/ Alias /dumb/ www/
Alias /auth/dumb/ www/auth/dumb/ Alias /auth/dumb/ www/auth/dumb/

127
t/t5534-push-signed.sh Executable file
View File

@ -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

View File

@ -12,6 +12,7 @@ if test -n "$NO_CURL"; then
fi fi
ROOT_PATH="$PWD" ROOT_PATH="$PWD"
. "$TEST_DIRECTORY"/lib-gpg.sh
. "$TEST_DIRECTORY"/lib-httpd.sh . "$TEST_DIRECTORY"/lib-httpd.sh
. "$TEST_DIRECTORY"/lib-terminal.sh . "$TEST_DIRECTORY"/lib-terminal.sh
start_httpd start_httpd
@ -338,5 +339,45 @@ test_expect_success CMDLINE_LIMIT 'push 2000 tags over http' '
run_with_limited_cmdline git push --mirror 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 stop_httpd
test_done test_done

View File

@ -813,7 +813,8 @@ rm -fr "$TRASH_DIRECTORY" || {
} }
HOME="$TRASH_DIRECTORY" HOME="$TRASH_DIRECTORY"
export HOME GNUPGHOME="$HOME/gnupg-home-not-used"
export HOME GNUPGHOME
if test -z "$TEST_NO_CREATE_REPO" if test -z "$TEST_NO_CREATE_REPO"
then then

20
tag.c
View File

@ -4,9 +4,6 @@
#include "tree.h" #include "tree.h"
#include "blob.h" #include "blob.h"
#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
#define PGP_MESSAGE "-----BEGIN PGP MESSAGE-----"
const char *tag_type = "tag"; const char *tag_type = "tag";
struct object *deref_tag(struct object *o, const char *warn, int warnlen) struct object *deref_tag(struct object *o, const char *warn, int warnlen)
@ -143,20 +140,3 @@ int parse_tag(struct tag *item)
free(data); free(data);
return ret; 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;
}

1
tag.h
View File

@ -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 int parse_tag(struct tag *item);
extern struct object *deref_tag(struct object *, const char *, int); extern struct object *deref_tag(struct object *, const char *, int);
extern struct object *deref_tag_noverify(struct object *); extern struct object *deref_tag_noverify(struct object *);
extern size_t parse_signature(const char *buf, unsigned long size);
#endif /* TAG_H */ #endif /* TAG_H */

View File

@ -260,7 +260,8 @@ static const char *unsupported_options[] = {
static const char *boolean_options[] = { static const char *boolean_options[] = {
TRANS_OPT_THIN, TRANS_OPT_THIN,
TRANS_OPT_KEEP, TRANS_OPT_KEEP,
TRANS_OPT_FOLLOWTAGS TRANS_OPT_FOLLOWTAGS,
TRANS_OPT_PUSH_CERT
}; };
static int set_helper_option(struct transport *transport, 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 (flags & TRANSPORT_PUSH_DRY_RUN) {
if (set_helper_option(transport, "dry-run", "true") != 0) if (set_helper_option(transport, "dry-run", "true") != 0)
die("helper %s does not support dry-run", data->name); 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'); strbuf_addch(&buf, '\n');
@ -860,6 +864,9 @@ static int push_refs_with_export(struct transport *transport,
if (flags & TRANSPORT_PUSH_DRY_RUN) { if (flags & TRANSPORT_PUSH_DRY_RUN) {
if (set_helper_option(transport, "dry-run", "true") != 0) if (set_helper_option(transport, "dry-run", "true") != 0)
die("helper %s does not support dry-run", data->name); 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) { if (flags & TRANSPORT_PUSH_FORCE) {

View File

@ -477,6 +477,9 @@ static int set_git_option(struct git_transport_options *opts,
die("transport: invalid depth option '%s'", value); die("transport: invalid depth option '%s'", value);
} }
return 0; return 0;
} else if (!strcmp(name, TRANS_OPT_PUSH_CERT)) {
opts->push_cert = !!value;
return 0;
} }
return 1; return 1;
} }
@ -820,6 +823,8 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re
args.progress = transport->progress; args.progress = transport->progress;
args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN); args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN);
args.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN); 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, ret = send_pack(&args, data->fd, data->conn, remote_refs,
&data->extra_have); &data->extra_have);

View File

@ -12,6 +12,7 @@ struct git_transport_options {
unsigned check_self_contained_and_connected : 1; unsigned check_self_contained_and_connected : 1;
unsigned self_contained_and_connected : 1; unsigned self_contained_and_connected : 1;
unsigned update_shallow : 1; unsigned update_shallow : 1;
unsigned push_cert : 1;
int depth; int depth;
const char *uploadpack; const char *uploadpack;
const char *receivepack; const char *receivepack;
@ -123,6 +124,7 @@ struct transport {
#define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256 #define TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND 256
#define TRANSPORT_PUSH_NO_HOOK 512 #define TRANSPORT_PUSH_NO_HOOK 512
#define TRANSPORT_PUSH_FOLLOW_TAGS 1024 #define TRANSPORT_PUSH_FOLLOW_TAGS 1024
#define TRANSPORT_PUSH_CERT 2048
#define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3) #define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
#define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x) #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 */ /* Accept refs that may update .git/shallow without --depth */
#define TRANS_OPT_UPDATE_SHALLOW "updateshallow" #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 * Returns 0 if the option was used, non-zero otherwise. Prints a
* message to stderr if the option is not used. * message to stderr if the option is not used.