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 <gitster@pobox.com>

Junio C Hamano committed Sep 5, 2014 at 10:46 UTC 5732373daacf9486a0db9741cf0de4e7a41b08b3
4 files changed +112 -12
Documentation/config.txt
+13
@@ -2049,6 +2049,19 @@ receive.certnonceseed::
2049 a "nonce" protected by HMAC using this string as a secret
2050 key.
2051
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 +
2065 receive.fsckObjects::
2066 If it is set to true, git-receive-pack will check all received
2067 objects. It will abort in the case of a malformed object or a
Documentation/git-receive-pack.txt
+13
@@ -89,6 +89,19 @@ the following environment variables:
89 "git push --signed" sent a bogus nonce.
90 `OK`;;
91 "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].
105
106 This hook is called before any refname is updated and before any
107 fast-forward checks are performed.
builtin/receive-pack.c
+79 -10
@@ -43,6 +43,8 @@ static int prefer_ofs_delta = 1;
43 static int auto_update_server_info;
44 static int auto_gc = 1;
45 static int fix_thin = 1;
46 +static int stateless_rpc;
47 +static const char *service_dir;
48 static const char *head_name;
49 static void *head_name_to_free;
50 static int sent_capabilities;
@@ -58,7 +60,10 @@ static const char *NONCE_UNSOLICITED = "UNSOLICITED";
60 static const char *NONCE_BAD = "BAD";
61 static const char *NONCE_MISSING = "MISSING";
62 static const char *NONCE_OK = "OK";
63 +static const char *NONCE_SLOP = "SLOP";
64 static const char *nonce_status;
65 +static long nonce_stamp_slop;
66 +static unsigned long nonce_stamp_slop_limit;
67
68 static enum deny_action parse_deny_action(const char *var, const char *value)
69 {
@@ -145,6 +150,11 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
150 if (strcmp(var, "receive.certnonceseed") == 0)
151 return git_config_string(&cert_nonce_seed, var, value);
152
153 + if (strcmp(var, "receive.certnonceslop") == 0) {
154 + nonce_stamp_slop_limit = git_config_ulong(var, value);
155 + return 0;
156 + }
157 +
158 return git_default_config(var, value, cb);
159 }
160
@@ -359,6 +369,8 @@ static char *find_header(const char *msg, size_t len, const char *key)
369 static const char *check_nonce(const char *buf, size_t len)
370 {
371 char *nonce = find_header(buf, len, "nonce");
372 + unsigned long stamp, ostamp;
373 + char *bohmac, *expect = NULL;
374 const char *retval = NONCE_BAD;
375
376 if (!nonce) {
@@ -372,11 +384,67 @@ static const char *check_nonce(const char *buf, size_t len)
384 goto leave;
385 }
386
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 + }
444
445 leave:
446 free(nonce);
447 + free(expect);
448 return retval;
449 }
450
@@ -426,6 +494,9 @@ static void prepare_push_cert_sha1(struct child_process *proc)
494 if (push_cert_nonce) {
495 argv_array_pushf(&env, "GIT_PUSH_CERT_NONCE=%s", push_cert_nonce);
496 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);
500 }
501 proc->env = env.argv;
502 }
@@ -1361,9 +1432,7 @@ static int delete_only(struct command *commands)
1432 int cmd_receive_pack(int argc, const char **argv, const char *prefix)
1433 {
1434 int advertise_refs = 0;
1364 - int stateless_rpc = 0;
1435 int i;
1366 - const char *dir = NULL;
1436 struct command *commands;
1437 struct sha1_array shallow = SHA1_ARRAY_INIT;
1438 struct sha1_array ref = SHA1_ARRAY_INIT;
@@ -1396,21 +1465,21 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
1465
1466 usage(receive_pack_usage);
1467 }
1399 - if (dir)
1468 + if (service_dir)
1469 usage(receive_pack_usage);
1401 - dir = arg;
1470 + service_dir = arg;
1471 }
1403 - if (!dir)
1472 + if (!service_dir)
1473 usage(receive_pack_usage);
1474
1475 setup_path();
1476
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);
1479
1480 git_config(receive_pack_config, NULL);
1481 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));
1483
1484 if (0 <= transfer_unpack_limit)
1485 unpack_limit = transfer_unpack_limit;
t/t5541-http-push-smart.sh
+7 -2
@@ -340,21 +340,26 @@ test_expect_success GPG 'push with post-receive to inspect certificate' '
340 SIGNER=${GIT_PUSH_CERT_SIGNER-nobody}
341 KEY=${GIT_PUSH_CERT_KEY-nokey}
342 STATUS=${GIT_PUSH_CERT_STATUS-nostatus}
343 + NONCE_STATUS=${GIT_PUSH_CERT_NONCE_STATUS-nononcestatus}
344 + NONCE=${GIT_PUSH_CERT_NONCE-nononce}
345 E_O_F
346 EOF
347
346 - git config receive.certnonceseed sekrit
348 + git config receive.certnonceseed sekrit &&
349 + git config receive.certnonceslop 30
350 ) &&
351 cd "$ROOT_PATH/test_repo_clone" &&
352 test_commit cert-test &&
353 git push --signed "$HTTPD_URL/smart/test_repo.git" &&
354 (
355 cd "$HTTPD_DOCUMENT_ROOT_PATH" &&
353 - cat <<-\EOF
356 + cat <<-\EOF &&
357 SIGNER=C O Mitter <committer@example.com>
358 KEY=13B6F51ECDDE430D
359 STATUS=G
360 + NONCE_STATUS=OK
361 EOF
362 + sed -n -e "s/^nonce /NONCE=/p" -e "/^$/q" push-cert
363 ) >expect &&
364 test_cmp expect "$HTTPD_DOCUMENT_ROOT_PATH/push-cert-status"
365 '