From 866cc988b51af57e0745ed4e74641c14fc83d434 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 25 May 2026 10:46:09 +1000 Subject: [PATCH] fix(dashboard-auth): use fixed-length sig suffix in stub token framing The stub auth provider's _sign/_unsign helpers joined payload and HMAC with a 'b"."' separator and recovered the parts via bytes.rsplit. HMAC-SHA256 digests are random bytes, so ~12% of the time the digest contains 0x2E ('.') and rsplit picks the wrong split point -- HMAC verification then spuriously rejects valid tokens. test_stub_refresh_round_trips was failing ~25% of the time in isolation because of this. Switch to a fixed-length suffix (32 bytes, sliced off in _unsign): no separator means no collision class. After the fix, 10/10 runs pass. --- tests/hermes_cli/conftest_dashboard_auth.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/hermes_cli/conftest_dashboard_auth.py b/tests/hermes_cli/conftest_dashboard_auth.py index 597c4b39b64..f06ec93f722 100644 --- a/tests/hermes_cli/conftest_dashboard_auth.py +++ b/tests/hermes_cli/conftest_dashboard_auth.py @@ -31,24 +31,35 @@ from hermes_cli.dashboard_auth.base import ( ) _STUB_SECRET = b"stub-test-secret-not-for-prod" +# Length of HMAC-SHA256 digest. We append this many trailing bytes of +# signature after ``raw`` in ``_sign``; ``_unsign`` slices them back off +# rather than splitting on a separator. (A separator byte chosen +# arbitrarily, e.g. ``b"."``, fails ~12% of the time when the HMAC +# digest happens to contain that byte — ``bytes.rsplit`` then splits at +# the wrong index and HMAC verification spuriously rejects the token.) +_SIG_LEN = hashlib.sha256().digest_size def _sign(payload: dict) -> str: """Produce a tamper-evident opaque token. - Not a real JWT — just a base64(JSON|HMAC-SHA256) blob with enough - structure to round-trip through verify_session. + Not a real JWT — just a base64(JSON || HMAC-SHA256) blob with enough + structure to round-trip through verify_session. The signature is + appended as a fixed-length suffix (no separator) so binary HMAC bytes + can't be confused with a delimiter. """ raw = json.dumps(payload, separators=(",", ":")).encode() sig = hmac.new(_STUB_SECRET, raw, hashlib.sha256).digest() - return base64.urlsafe_b64encode(raw + b"." + sig).decode() + return base64.urlsafe_b64encode(raw + sig).decode() def _unsign(token: str) -> dict | None: """Inverse of ``_sign``; returns None on any tamper/decode failure.""" try: blob = base64.urlsafe_b64decode(token.encode()) - raw, sig = blob.rsplit(b".", 1) + if len(blob) <= _SIG_LEN: + return None + raw, sig = blob[:-_SIG_LEN], blob[-_SIG_LEN:] expected = hmac.new(_STUB_SECRET, raw, hashlib.sha256).digest() if not hmac.compare_digest(sig, expected): return None