Skip to content

Commit

Permalink
receive-pack: verify push options in cert
Browse files Browse the repository at this point in the history
In commit f6a4e61 ("push: accept push options", 2016-07-14), send-pack
was taught to include push options both within the signed cert (if the
push is a signed push) and outside the signed cert; however,
receive-pack ignores push options within the cert, only handling push
options outside the cert.

Teach receive-pack, in the case that push options are provided for a
signed push, to verify that the push options both within the cert and
outside the cert are consistent.

This sets in stone the requirement that send-pack redundantly send its
push options in 2 places, but I think that this is better than the
alternatives. Sending push options only within the cert is
backwards-incompatible with existing Git servers (which read push
options only from outside the cert), and sending push options only
outside the cert means that the push options are not signed for.

Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
jonathantanmy authored and gitster committed May 10, 2017
1 parent b7b744f commit cbaf82c
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 8 deletions.
32 changes: 26 additions & 6 deletions Documentation/technical/pack-protocol.txt
Expand Up @@ -468,13 +468,10 @@ that it wants to update, it sends a line listing the obj-id currently on
the server, the obj-id the client would like to update it to and the name
of the reference.

This list is followed by a flush-pkt. Then the push options are transmitted
one per packet followed by another flush-pkt. After that the packfile that
should contain all the objects that the server will need to complete the new
references will be sent.
This list is followed by a flush-pkt.

----
update-request = *shallow ( command-list | push-cert ) [packfile]
update-requests = *shallow ( command-list | push-cert )

shallow = PKT-LINE("shallow" SP obj-id)

Expand All @@ -495,12 +492,35 @@ references will be sent.
PKT-LINE("pusher" SP ident LF)
PKT-LINE("pushee" SP url LF)
PKT-LINE("nonce" SP nonce LF)
*PKT-LINE("push-option" SP push-option LF)
PKT-LINE(LF)
*PKT-LINE(command LF)
*PKT-LINE(gpg-signature-lines LF)
PKT-LINE("push-cert-end" LF)

packfile = "PACK" 28*(OCTET)
push-option = 1*( VCHAR | SP )
----

If the server has advertised the 'push-options' capability and the client has
specified 'push-options' as part of the capability list above, the client then
sends its push options followed by a flush-pkt.

----
push-options = *PKT-LINE(push-option) flush-pkt
----

For backwards compatibility with older Git servers, if the client sends a push
cert and push options, it MUST send its push options both embedded within the
push cert and after the push cert. (Note that the push options within the cert
are prefixed, but the push options after the cert are not.) Both these lists
MUST be the same, modulo the prefix.

After that the packfile that
should contain all the objects that the server will need to complete the new
references will be sent.

----
packfile = "PACK" 28*(OCTET)
----

If the receiving end does not support delete-refs, the sending end MUST
Expand Down
51 changes: 49 additions & 2 deletions builtin/receive-pack.c
Expand Up @@ -470,7 +470,8 @@ static char *prepare_push_cert_nonce(const char *path, unsigned long stamp)
* 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)
static char *find_header(const char *msg, size_t len, const char *key,
const char **next_line)
{
int key_len = strlen(key);
const char *line = msg;
Expand All @@ -483,6 +484,8 @@ static char *find_header(const char *msg, size_t len, const char *key)
if (line + key_len < eol &&
!memcmp(line, key, key_len) && line[key_len] == ' ') {
int offset = key_len + 1;
if (next_line)
*next_line = *eol ? eol + 1 : eol;
return xmemdupz(line + offset, (eol - line) - offset);
}
line = *eol ? eol + 1 : NULL;
Expand All @@ -492,7 +495,7 @@ static char *find_header(const char *msg, size_t len, const char *key)

static const char *check_nonce(const char *buf, size_t len)
{
char *nonce = find_header(buf, len, "nonce");
char *nonce = find_header(buf, len, "nonce", NULL);
unsigned long stamp, ostamp;
char *bohmac, *expect = NULL;
const char *retval = NONCE_BAD;
Expand Down Expand Up @@ -572,6 +575,45 @@ static const char *check_nonce(const char *buf, size_t len)
return retval;
}

/*
* Return 1 if there is no push_cert or if the push options in push_cert are
* the same as those in the argument; 0 otherwise.
*/
static int check_cert_push_options(const struct string_list *push_options)
{
const char *buf = push_cert.buf;
int len = push_cert.len;

char *option;
const char *next_line;
int options_seen = 0;

int retval = 1;

if (!len)
return 1;

while ((option = find_header(buf, len, "push-option", &next_line))) {
len -= (next_line - buf);
buf = next_line;
options_seen++;
if (options_seen > push_options->nr
|| strcmp(option,
push_options->items[options_seen - 1].string)) {
retval = 0;
goto leave;
}
free(option);
}

if (options_seen != push_options->nr)
retval = 0;

leave:
free(option);
return retval;
}

static void prepare_push_cert_sha1(struct child_process *proc)
{
static int already_done;
Expand Down Expand Up @@ -1924,6 +1966,11 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)

if (use_push_options)
read_push_options(&push_options);
if (!check_cert_push_options(&push_options)) {
struct command *cmd;
for (cmd = commands; cmd; cmd = cmd->next)
cmd->error_string = "inconsistent push options";
}

prepare_shallow_info(&si, &shallow);
if (!si.nr_ours && !si.nr_theirs)
Expand Down
37 changes: 37 additions & 0 deletions t/t5534-push-signed.sh
Expand Up @@ -124,6 +124,43 @@ test_expect_success GPG 'signed push sends push certificate' '
test_cmp expect dst/push-cert-status
'

test_expect_success GPG 'inconsistent push options in signed push not allowed' '
# First, invoke receive-pack with dummy input to obtain its preamble.
prepare_dst &&
git -C dst config receive.certnonceseed sekrit &&
git -C dst config receive.advertisepushoptions 1 &&
printf xxxx | test_might_fail git receive-pack dst >preamble &&
# Then, invoke push. Simulate a receive-pack that sends the preamble we
# obtained, followed by a dummy packet.
write_script myscript <<-\EOF &&
cat preamble &&
printf xxxx &&
cat >push
EOF
test_might_fail git push --push-option="foo" --push-option="bar" \
--receive-pack="\"$(pwd)/myscript\"" --signed dst --delete ff &&
# Replay the push output on a fresh dst, checking that ff is truly
# deleted.
prepare_dst &&
git -C dst config receive.certnonceseed sekrit &&
git -C dst config receive.advertisepushoptions 1 &&
git receive-pack dst <push &&
test_must_fail git -C dst rev-parse ff &&
# Tweak the push output to make the push option outside the cert
# different, then replay it on a fresh dst, checking that ff is not
# deleted.
perl -pe "s/([^ ])bar/\$1baz/" push >push.tweak &&
prepare_dst &&
git -C dst config receive.certnonceseed sekrit &&
git -C dst config receive.advertisepushoptions 1 &&
git receive-pack dst <push.tweak >out &&
git -C dst rev-parse ff &&
grep "inconsistent push options" out
'

test_expect_success GPG 'fail without key and heed user.signingkey' '
prepare_dst &&
mkdir -p dst/.git/hooks &&
Expand Down

0 comments on commit cbaf82c

Please sign in to comment.