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.
This commit is contained in:
Ben 2026-05-25 10:46:09 +10:00 committed by Teknium
parent c598076b76
commit 866cc988b5

View file

@ -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