Skip to content

Commit cbaf82c

Browse files
jonathantanmygitster
authored andcommitted
receive-pack: verify push options in cert
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent b7b744f commit cbaf82c

File tree

3 files changed

+112
-8
lines changed

3 files changed

+112
-8
lines changed

Documentation/technical/pack-protocol.txt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -468,13 +468,10 @@ that it wants to update, it sends a line listing the obj-id currently on
468468
the server, the obj-id the client would like to update it to and the name
469469
of the reference.
470470

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

476473
----
477-
update-request = *shallow ( command-list | push-cert ) [packfile]
474+
update-requests = *shallow ( command-list | push-cert )
478475

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

@@ -495,12 +492,35 @@ references will be sent.
495492
PKT-LINE("pusher" SP ident LF)
496493
PKT-LINE("pushee" SP url LF)
497494
PKT-LINE("nonce" SP nonce LF)
495+
*PKT-LINE("push-option" SP push-option LF)
498496
PKT-LINE(LF)
499497
*PKT-LINE(command LF)
500498
*PKT-LINE(gpg-signature-lines LF)
501499
PKT-LINE("push-cert-end" LF)
502500

503-
packfile = "PACK" 28*(OCTET)
501+
push-option = 1*( VCHAR | SP )
502+
----
503+
504+
If the server has advertised the 'push-options' capability and the client has
505+
specified 'push-options' as part of the capability list above, the client then
506+
sends its push options followed by a flush-pkt.
507+
508+
----
509+
push-options = *PKT-LINE(push-option) flush-pkt
510+
----
511+
512+
For backwards compatibility with older Git servers, if the client sends a push
513+
cert and push options, it MUST send its push options both embedded within the
514+
push cert and after the push cert. (Note that the push options within the cert
515+
are prefixed, but the push options after the cert are not.) Both these lists
516+
MUST be the same, modulo the prefix.
517+
518+
After that the packfile that
519+
should contain all the objects that the server will need to complete the new
520+
references will be sent.
521+
522+
----
523+
packfile = "PACK" 28*(OCTET)
504524
----
505525

506526
If the receiving end does not support delete-refs, the sending end MUST

builtin/receive-pack.c

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,8 @@ static char *prepare_push_cert_nonce(const char *path, unsigned long stamp)
470470
* after dropping "_commit" from its name and possibly moving it out
471471
* of commit.c
472472
*/
473-
static char *find_header(const char *msg, size_t len, const char *key)
473+
static char *find_header(const char *msg, size_t len, const char *key,
474+
const char **next_line)
474475
{
475476
int key_len = strlen(key);
476477
const char *line = msg;
@@ -483,6 +484,8 @@ static char *find_header(const char *msg, size_t len, const char *key)
483484
if (line + key_len < eol &&
484485
!memcmp(line, key, key_len) && line[key_len] == ' ') {
485486
int offset = key_len + 1;
487+
if (next_line)
488+
*next_line = *eol ? eol + 1 : eol;
486489
return xmemdupz(line + offset, (eol - line) - offset);
487490
}
488491
line = *eol ? eol + 1 : NULL;
@@ -492,7 +495,7 @@ static char *find_header(const char *msg, size_t len, const char *key)
492495

493496
static const char *check_nonce(const char *buf, size_t len)
494497
{
495-
char *nonce = find_header(buf, len, "nonce");
498+
char *nonce = find_header(buf, len, "nonce", NULL);
496499
unsigned long stamp, ostamp;
497500
char *bohmac, *expect = NULL;
498501
const char *retval = NONCE_BAD;
@@ -572,6 +575,45 @@ static const char *check_nonce(const char *buf, size_t len)
572575
return retval;
573576
}
574577

578+
/*
579+
* Return 1 if there is no push_cert or if the push options in push_cert are
580+
* the same as those in the argument; 0 otherwise.
581+
*/
582+
static int check_cert_push_options(const struct string_list *push_options)
583+
{
584+
const char *buf = push_cert.buf;
585+
int len = push_cert.len;
586+
587+
char *option;
588+
const char *next_line;
589+
int options_seen = 0;
590+
591+
int retval = 1;
592+
593+
if (!len)
594+
return 1;
595+
596+
while ((option = find_header(buf, len, "push-option", &next_line))) {
597+
len -= (next_line - buf);
598+
buf = next_line;
599+
options_seen++;
600+
if (options_seen > push_options->nr
601+
|| strcmp(option,
602+
push_options->items[options_seen - 1].string)) {
603+
retval = 0;
604+
goto leave;
605+
}
606+
free(option);
607+
}
608+
609+
if (options_seen != push_options->nr)
610+
retval = 0;
611+
612+
leave:
613+
free(option);
614+
return retval;
615+
}
616+
575617
static void prepare_push_cert_sha1(struct child_process *proc)
576618
{
577619
static int already_done;
@@ -1924,6 +1966,11 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
19241966

19251967
if (use_push_options)
19261968
read_push_options(&push_options);
1969+
if (!check_cert_push_options(&push_options)) {
1970+
struct command *cmd;
1971+
for (cmd = commands; cmd; cmd = cmd->next)
1972+
cmd->error_string = "inconsistent push options";
1973+
}
19271974

19281975
prepare_shallow_info(&si, &shallow);
19291976
if (!si.nr_ours && !si.nr_theirs)

t/t5534-push-signed.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,43 @@ test_expect_success GPG 'signed push sends push certificate' '
124124
test_cmp expect dst/push-cert-status
125125
'
126126

127+
test_expect_success GPG 'inconsistent push options in signed push not allowed' '
128+
# First, invoke receive-pack with dummy input to obtain its preamble.
129+
prepare_dst &&
130+
git -C dst config receive.certnonceseed sekrit &&
131+
git -C dst config receive.advertisepushoptions 1 &&
132+
printf xxxx | test_might_fail git receive-pack dst >preamble &&
133+
134+
# Then, invoke push. Simulate a receive-pack that sends the preamble we
135+
# obtained, followed by a dummy packet.
136+
write_script myscript <<-\EOF &&
137+
cat preamble &&
138+
printf xxxx &&
139+
cat >push
140+
EOF
141+
test_might_fail git push --push-option="foo" --push-option="bar" \
142+
--receive-pack="\"$(pwd)/myscript\"" --signed dst --delete ff &&
143+
144+
# Replay the push output on a fresh dst, checking that ff is truly
145+
# deleted.
146+
prepare_dst &&
147+
git -C dst config receive.certnonceseed sekrit &&
148+
git -C dst config receive.advertisepushoptions 1 &&
149+
git receive-pack dst <push &&
150+
test_must_fail git -C dst rev-parse ff &&
151+
152+
# Tweak the push output to make the push option outside the cert
153+
# different, then replay it on a fresh dst, checking that ff is not
154+
# deleted.
155+
perl -pe "s/([^ ])bar/\$1baz/" push >push.tweak &&
156+
prepare_dst &&
157+
git -C dst config receive.certnonceseed sekrit &&
158+
git -C dst config receive.advertisepushoptions 1 &&
159+
git receive-pack dst <push.tweak >out &&
160+
git -C dst rev-parse ff &&
161+
grep "inconsistent push options" out
162+
'
163+
127164
test_expect_success GPG 'fail without key and heed user.signingkey' '
128165
prepare_dst &&
129166
mkdir -p dst/.git/hooks &&

0 commit comments

Comments
 (0)