Skip to content

Commit 5732373

Browse files
committed
signed push: allow stale nonce in stateless mode
When operating with the stateless RPC mode, we will receive a nonce issued by another instance of us that advertised our capability and refs some time ago. Update the logic to check received nonce to detect this case, compute how much time has passed since the nonce was issued and report the status with a new environment variable GIT_PUSH_CERT_NONCE_SLOP to the hooks. GIT_PUSH_CERT_NONCE_STATUS will report "SLOP" in such a case. The hooks are free to decide how large a slop it is willing to accept. Strictly speaking, the "nonce" is not really a "nonce" anymore in the stateless RPC mode, as it will happily take any "nonce" issued by it (which is protected by HMAC and its secret key) as long as it is fresh enough. The degree of this security degradation, relative to the native protocol, is about the same as the "we make sure that the 'git push' decided to update our refs with new objects based on the freshest observation of our refs by making sure the values they claim the original value of the refs they ask us to update exactly match the current state" security is loosened to accomodate the stateless RPC mode in the existing code without this series, so there is no need for those who are already using smart HTTP to push to their repositories to be alarmed any more than they already are. In addition, the server operator can set receive.certnonceslop configuration variable to specify how stale a nonce can be (in seconds). When this variable is set, and if the nonce received in the certificate that passes the HMAC check was less than that many seconds old, hooks are given "OK" in GIT_PUSH_CERT_NONCE_STATUS (instead of "SLOP") and the received nonce value is given in GIT_PUSH_CERT_NONCE, which makes it easier for a simple-minded hook to check if the certificate we received is recent enough. Signed-off-by: Junio C Hamano <[email protected]>
1 parent 0ea47f9 commit 5732373

File tree

4 files changed

+112
-12
lines changed

4 files changed

+112
-12
lines changed

Documentation/config.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,19 @@ receive.certnonceseed::
20492049
a "nonce" protected by HMAC using this string as a secret
20502050
key.
20512051

2052+
receive.certnonceslop::
2053+
When a `git push --signed` sent a push certificate with a
2054+
"nonce" that was issued by a receive-pack serving the same
2055+
repository within this many seconds, export the "nonce"
2056+
found in the certificate to `GIT_PUSH_CERT_NONCE` to the
2057+
hooks (instead of what the receive-pack asked the sending
2058+
side to include). This may allow writing checks in
2059+
`pre-receive` and `post-receive` a bit easier. Instead of
2060+
checking `GIT_PUSH_CERT_NONCE_SLOP` environment variable
2061+
that records by how many seconds the nonce is stale to
2062+
decide if they want to accept the certificate, they only
2063+
can check `GIT_PUSH_CERT_NONCE_STATUS` is `OK`.
2064+
20522065
receive.fsckObjects::
20532066
If it is set to true, git-receive-pack will check all received
20542067
objects. It will abort in the case of a malformed object or a

Documentation/git-receive-pack.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ the following environment variables:
8989
"git push --signed" sent a bogus nonce.
9090
`OK`;;
9191
"git push --signed" sent the nonce we asked it to send.
92+
`SLOP`;;
93+
"git push --signed" sent a nonce different from what we
94+
asked it to send now, but in a previous session. See
95+
`GIT_PUSH_CERT_NONCE_SLOP` environment variable.
96+
97+
`GIT_PUSH_CERT_NONCE_SLOP`::
98+
"git push --signed" sent a nonce different from what we
99+
asked it to send now, but in a different session whose
100+
starting time is different by this many seconds from the
101+
current session. Only meaningful when
102+
`GIT_PUSH_CERT_NONCE_STATUS` says `SLOP`.
103+
Also read about `receive.certnonceslop` variable in
104+
linkgit:git-config[1].
92105

93106
This hook is called before any refname is updated and before any
94107
fast-forward checks are performed.

builtin/receive-pack.c

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ static int prefer_ofs_delta = 1;
4343
static int auto_update_server_info;
4444
static int auto_gc = 1;
4545
static int fix_thin = 1;
46+
static int stateless_rpc;
47+
static const char *service_dir;
4648
static const char *head_name;
4749
static void *head_name_to_free;
4850
static int sent_capabilities;
@@ -58,7 +60,10 @@ static const char *NONCE_UNSOLICITED = "UNSOLICITED";
5860
static const char *NONCE_BAD = "BAD";
5961
static const char *NONCE_MISSING = "MISSING";
6062
static const char *NONCE_OK = "OK";
63+
static const char *NONCE_SLOP = "SLOP";
6164
static const char *nonce_status;
65+
static long nonce_stamp_slop;
66+
static unsigned long nonce_stamp_slop_limit;
6267

6368
static enum deny_action parse_deny_action(const char *var, const char *value)
6469
{
@@ -145,6 +150,11 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
145150
if (strcmp(var, "receive.certnonceseed") == 0)
146151
return git_config_string(&cert_nonce_seed, var, value);
147152

153+
if (strcmp(var, "receive.certnonceslop") == 0) {
154+
nonce_stamp_slop_limit = git_config_ulong(var, value);
155+
return 0;
156+
}
157+
148158
return git_default_config(var, value, cb);
149159
}
150160

@@ -359,6 +369,8 @@ static char *find_header(const char *msg, size_t len, const char *key)
359369
static const char *check_nonce(const char *buf, size_t len)
360370
{
361371
char *nonce = find_header(buf, len, "nonce");
372+
unsigned long stamp, ostamp;
373+
char *bohmac, *expect = NULL;
362374
const char *retval = NONCE_BAD;
363375

364376
if (!nonce) {
@@ -372,11 +384,67 @@ static const char *check_nonce(const char *buf, size_t len)
372384
goto leave;
373385
}
374386

375-
/* returned nonce MUST match what we gave out earlier */
376-
retval = NONCE_BAD;
387+
if (!stateless_rpc) {
388+
/* returned nonce MUST match what we gave out earlier */
389+
retval = NONCE_BAD;
390+
goto leave;
391+
}
392+
393+
/*
394+
* In stateless mode, we may be receiving a nonce issued by
395+
* another instance of the server that serving the same
396+
* repository, and the timestamps may not match, but the
397+
* nonce-seed and dir should match, so we can recompute and
398+
* report the time slop.
399+
*
400+
* In addition, when a nonce issued by another instance has
401+
* timestamp within receive.certnonceslop seconds, we pretend
402+
* as if we issued that nonce when reporting to the hook.
403+
*/
404+
405+
/* nonce is concat(<seconds-since-epoch>, "-", <hmac>) */
406+
if (*nonce <= '0' || '9' < *nonce) {
407+
retval = NONCE_BAD;
408+
goto leave;
409+
}
410+
stamp = strtoul(nonce, &bohmac, 10);
411+
if (bohmac == nonce || bohmac[0] != '-') {
412+
retval = NONCE_BAD;
413+
goto leave;
414+
}
415+
416+
expect = prepare_push_cert_nonce(service_dir, stamp);
417+
if (strcmp(expect, nonce)) {
418+
/* Not what we would have signed earlier */
419+
retval = NONCE_BAD;
420+
goto leave;
421+
}
422+
423+
/*
424+
* By how many seconds is this nonce stale? Negative value
425+
* would mean it was issued by another server with its clock
426+
* skewed in the future.
427+
*/
428+
ostamp = strtoul(push_cert_nonce, NULL, 10);
429+
nonce_stamp_slop = (long)ostamp - (long)stamp;
430+
431+
if (nonce_stamp_slop_limit &&
432+
abs(nonce_stamp_slop) <= nonce_stamp_slop_limit) {
433+
/*
434+
* Pretend as if the received nonce (which passes the
435+
* HMAC check, so it is not a forged by third-party)
436+
* is what we issued.
437+
*/
438+
free((void *)push_cert_nonce);
439+
push_cert_nonce = xstrdup(nonce);
440+
retval = NONCE_OK;
441+
} else {
442+
retval = NONCE_SLOP;
443+
}
377444

378445
leave:
379446
free(nonce);
447+
free(expect);
380448
return retval;
381449
}
382450

@@ -426,6 +494,9 @@ static void prepare_push_cert_sha1(struct child_process *proc)
426494
if (push_cert_nonce) {
427495
argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE=%s", push_cert_nonce);
428496
argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_STATUS=%s", nonce_status);
497+
if (nonce_status == NONCE_SLOP)
498+
argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE_SLOP=%ld",
499+
nonce_stamp_slop);
429500
}
430501
proc->env = env.argv;
431502
}
@@ -1361,9 +1432,7 @@ static int delete_only(struct command *commands)
13611432
int cmd_receive_pack(int argc, const char **argv, const char *prefix)
13621433
{
13631434
int advertise_refs = 0;
1364-
int stateless_rpc = 0;
13651435
int i;
1366-
const char *dir = NULL;
13671436
struct command *commands;
13681437
struct sha1_array shallow = SHA1_ARRAY_INIT;
13691438
struct sha1_array ref = SHA1_ARRAY_INIT;
@@ -1396,21 +1465,21 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
13961465

13971466
usage(receive_pack_usage);
13981467
}
1399-
if (dir)
1468+
if (service_dir)
14001469
usage(receive_pack_usage);
1401-
dir = arg;
1470+
service_dir = arg;
14021471
}
1403-
if (!dir)
1472+
if (!service_dir)
14041473
usage(receive_pack_usage);
14051474

14061475
setup_path();
14071476

1408-
if (!enter_repo(dir, 0))
1409-
die("'%s' does not appear to be a git repository", dir);
1477+
if (!enter_repo(service_dir, 0))
1478+
die("'%s' does not appear to be a git repository", service_dir);
14101479

14111480
git_config(receive_pack_config, NULL);
14121481
if (cert_nonce_seed)
1413-
push_cert_nonce = prepare_push_cert_nonce(dir, time(NULL));
1482+
push_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL));
14141483

14151484
if (0 <= transfer_unpack_limit)
14161485
unpack_limit = transfer_unpack_limit;

t/t5541-http-push-smart.sh

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,21 +340,26 @@ test_expect_success GPG 'push with post-receive to inspect certificate' '
340340
SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}
341341
KEY=${GIT_PUSH_CERT_KEY-nokey}
342342
STATUS=${GIT_PUSH_CERT_STATUS-nostatus}
343+
NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}
344+
NONCE=${GIT_PUSH_CERT_NONCE-nononce}
343345
E_O_F
344346
EOF
345347
346-
git config receive.certnonceseed sekrit
348+
git config receive.certnonceseed sekrit &&
349+
git config receive.certnonceslop 30
347350
) &&
348351
cd "$ROOT_PATH/test_repo_clone" &&
349352
test_commit cert-test &&
350353
git push --signed "$HTTPD_URL/smart/test_repo.git" &&
351354
(
352355
cd "$HTTPD_DOCUMENT_ROOT_PATH" &&
353-
cat <<-\EOF
356+
cat <<-\EOF &&
354357
SIGNER=C O Mitter <[email protected]>
355358
KEY=13B6F51ECDDE430D
356359
STATUS=G
360+
NONCE_STATUS=OK
357361
EOF
362+
sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" push-cert
358363
) >expect &&
359364
test_cmp expect "$HTTPD_DOCUMENT_ROOT_PATH/push-cert-status"
360365
'

0 commit comments

Comments
 (0)