mirror of
https://github.com/git/git.git
synced 2024-11-23 18:05:29 +08:00
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:
commit
fb06b5280e
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
-------------
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
};
|
||||
|
||||
|
@ -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());
|
||||
} 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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
36
commit.c
36
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;
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
||||
/*
|
||||
* possible "result":
|
||||
* 0 (not checked)
|
||||
* N (checked but no further result)
|
||||
* U (untrusted good)
|
||||
* G (good)
|
||||
* B (bad) */
|
||||
* 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 *);
|
||||
|
@ -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)
|
||||
|
197
send-pack.c
197
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);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
new_refs = 0;
|
||||
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;
|
||||
|
||||
/* 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:
|
||||
if (!ref_update_to_be_sent(ref, args))
|
||||
continue;
|
||||
default:
|
||||
; /* do nothing */
|
||||
}
|
||||
|
||||
if (ref->deletion && !allow_deleting_refs) {
|
||||
ref->status = REF_STATUS_REJECT_NODELETE;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ref->deletion)
|
||||
new_refs++;
|
||||
|
||||
if (args->dry_run) {
|
||||
ref->status = REF_STATUS_OK;
|
||||
} 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)) {
|
||||
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%s%s%s%s",
|
||||
"%s %s %s%c%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
|
||||
cap_buf.buf);
|
||||
cmds_sent = 1;
|
||||
} 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);
|
||||
}
|
||||
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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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/
|
||||
|
127
t/t5534-push-signed.sh
Executable file
127
t/t5534-push-signed.sh
Executable 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
|
@ -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
|
||||
|
@ -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
|
||||
|
20
tag.c
20
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;
|
||||
}
|
||||
|
1
tag.h
1
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 */
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user