From 5355095c48711598fb2171a6c6d47ce7c05e10e3 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Thu, 20 Aug 2026 03:08:06 -0400 Subject: [PATCH] Compare node API keys in constant time on /upload /upload compared the per-node API key with a plain !=, which short-circuits on the first differing byte and so leaks a little information about how much of a guess was correct. The reason to fix it is less the timing channel itself -- an HTTP round trip is noisy -- than the inconsistency: enrollment.py and dynsec.py both went out of their way to use secrets.compare_digest for the same class of credential, so the codebase contradicted itself on whether this mattered. Now it does not. Also coalesces a missing api_key field to "" so compare_digest is never handed None, which would raise TypeError and turn a malformed node_keys document into a 500 instead of a 401. Closes logan/server-26#12 --- drb-c2-core/app/routers/upload.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drb-c2-core/app/routers/upload.py b/drb-c2-core/app/routers/upload.py index 063f044..b62707c 100644 --- a/drb-c2-core/app/routers/upload.py +++ b/drb-c2-core/app/routers/upload.py @@ -1,3 +1,4 @@ +import secrets from typing import Optional from datetime import datetime, timezone from fastapi import APIRouter, BackgroundTasks, UploadFile, File, Form, HTTPException, Security @@ -36,7 +37,11 @@ async def upload_call_audio( if not key_doc: logger.warning(f"Upload 401: no key_doc in Firestore for node_id={node_id!r}") raise HTTPException(401, "Invalid node API key") - if key_doc.get("api_key") != credentials.credentials: + # compare_digest, not !=, so the comparison cost does not depend on how many + # leading characters matched. enrollment.py and dynsec.py were explicit about + # this for the same class of credential; this route was the odd one out. + stored_key = key_doc.get("api_key") or "" + if not secrets.compare_digest(stored_key, credentials.credentials): logger.warning( f"Upload 401: key mismatch for node_id={node_id!r} " f"(received prefix: {credentials.credentials[:8]}...)"