33eb806d3707661d8a4d9af1e0673f90578d86bc
max
  Mon Sep 21 06:12:21 2026 -0700
hgHubConnect: the api key sync has to answer before the cart, and its signature needed a timestamp, refs #38323

Six things, all in the syncHubApiKeys path, which is still off everywhere.

The sync never reached its handler on a real mirror.  It was registered as
a cartJson command, so building the cart ran forceUserIdOrCaptcha() first;
a peer's request carries no hguid cookie and no apiKey= variable, so any
site with cloudFlareSiteKey set -- which is every site that needs api keys
in the first place -- answered it with a captcha page.  Only hgwdev, where
the captcha is commented out, ever ran the handler, which is why this was
not caught earlier.  doApiKeySyncIfRequested() now answers the request in
main() before any cart exists.  That also stops each sync leaving a junk
userDb and sessionDb row behind: measured, 15 syncs now create none.

The signature is HMAC-SHA256 over the salt instead of md5(salt-user-key),
so it does not rest on md5 resisting length extension, and it is compared
in constant time.  It now covers a timestamp too, and a peer refuses
anything signed more than HUB_APIKEY_SYNC_WINDOW (300s) either side of
now, so a captured sync cannot be replayed later to reinstate a key its
owner has since revoked.  The fields are joined with newlines and a
newline in a userName or apiKey is refused, so one signature cannot cover
two different splits of the same string.

syncApiKeyToOtherNodes() is called inside an errCatch of its own now.  It
runs after the local key is already written and in the response, so an
errAbort there (login.cookieSalt unset) used to reach cartJsonExecute's
outer catch, which threw the response away -- the user saw a failure over
a key that was sitting in the database.

geoMirrorNotifyOtherNodes() returns each peer's response instead of
discarding it, and uses netUrlMustOpenPastHeader, which errAborts on
anything but a 200 and hands back the body alone.  A peer answering 'bad
signature' or 'not enabled on this site' is no longer indistinguishable
from success; it is logged to error_log, not warn()ed at the person who
clicked the button.

Verified against the built CGI run as Apache runs it, with the captcha
turned on: a signed sync is accepted and writes the row, a signed revoke
removes it, and a replay, a swapped userName, a forged signature and
malformed json are all refused.  hubSpaceKeysTester covers the signature
rules; it still cannot run its database half until hgcentralregress gets
the UPDATE and DELETE grant.

diff --git src/lib/hmac.c src/lib/hmac.c
index 1b9d3311c6d..5944a655881 100644
--- src/lib/hmac.c
+++ src/lib/hmac.c
@@ -1,39 +1,51 @@
 /* Calculate an openssl keyed-hash message authentication code (HMAC) */
 // You may use other openssl hash engines. e.g EVP_md5(), EVP_sha224,
 // EVP_sha512, etc
 // Be careful of the length of string with the choosen hash engine.
 // SHA1 needed 20 characters, MD5 needed 16 characters.
 // Change the length accordingly with your choosen hash engine
 
 /* Copyright (C) 2013 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "openssl/hmac.h"
 #include "openssl/evp.h"
 #include "common.h"
 
 char *hmacSha1(char *key, char *data)
 /* Calculate a openssl SHA1 keyed-hash message authentication code (HMAC) */
 {
 unsigned char* digest;
 digest=HMAC(EVP_sha1(), key, strlen(key), (unsigned char*)data, strlen(data), NULL, NULL);
 char hmacStr[40+1];   /* the last sprintf writes its terminating zero at [40] */
 int i;
 for(i = 0; i < 20; i++)
     sprintf(&hmacStr[i*2], "%02x", (unsigned int)digest[i]);
 return cloneString(hmacStr);
 }
 
+char *hmacSha256(char *key, char *data)
+/* Calculate a openssl SHA256 keyed-hash message authentication code (HMAC) */
+{
+unsigned char* digest;
+digest=HMAC(EVP_sha256(), key, strlen(key), (unsigned char*)data, strlen(data), NULL, NULL);
+char hmacStr[64+1];   /* the last sprintf writes its terminating zero at [64] */
+int i;
+for(i = 0; i < 32; i++)
+    sprintf(&hmacStr[i*2], "%02x", (unsigned int)digest[i]);
+return cloneString(hmacStr);
+}
+
 char *hmacMd5(char *key, char *data)
 /* Calculate a openssl MD5 keyed-hash message authentication code (HMAC) */
 {
 unsigned char* digest;
 digest=HMAC(EVP_md5(), key, strlen(key), (unsigned char*)data, strlen(data), NULL, NULL);
 //printf("Raw mdr digest: %s\n", digest);
 char hmacStr[32+1];   /* the last sprintf writes its terminating zero at [32] */
 int i;
 for(i = 0; i < 16; i++)
     sprintf(&hmacStr[i*2], "%02x", (unsigned int)digest[i]);
 return cloneString(hmacStr);
 }