From aaa94d4b07261aa01a3a6c3126680d10c57156d1 Mon Sep 17 00:00:00 2001 From: Hamza Date: Mon, 11 May 2026 22:48:36 +0100 Subject: [PATCH] fix: use constant-time Sophia governor admin auth --- node/sophia_governor.py | 3 +- node/tests/test_sophia_governor.py | 48 +++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/node/sophia_governor.py b/node/sophia_governor.py index b79ff2e0a..284049773 100644 --- a/node/sophia_governor.py +++ b/node/sophia_governor.py @@ -16,6 +16,7 @@ from __future__ import annotations +import hmac import json import logging import os @@ -938,7 +939,7 @@ def _is_admin(req) -> bool: if not required: return False provided = (req.headers.get("X-Admin-Key") or req.headers.get("X-API-Key") or "").strip() - return bool(provided and provided == required) + return bool(provided and hmac.compare_digest(provided, required)) @app.route("/sophia/governor/status", methods=["GET"]) def sophia_governor_status(): diff --git a/node/tests/test_sophia_governor.py b/node/tests/test_sophia_governor.py index a11748c9a..8e24afb5b 100644 --- a/node/tests/test_sophia_governor.py +++ b/node/tests/test_sophia_governor.py @@ -1,6 +1,8 @@ +import gc import os import sqlite3 import tempfile +import time import types import pytest @@ -10,6 +12,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +import sophia_governor from sophia_governor import ( ROUTE_IMMEDIATE_PHONE_HOME, ROUTE_LOCAL_ONLY, @@ -37,7 +40,13 @@ def tmp_db(): db_path = handle.name init_sophia_governor_schema(db_path) yield db_path - os.unlink(db_path) + for _ in range(5): + try: + os.unlink(db_path) + break + except PermissionError: + gc.collect() + time.sleep(0.05) @pytest.fixture @@ -177,6 +186,43 @@ def test_governor_endpoints_require_admin_for_manual_review(client): assert response.status_code == 401 +def test_governor_admin_auth_uses_constant_time_compare(client, monkeypatch): + """Admin-gated governor endpoints compare configured keys with hmac.compare_digest.""" + calls = [] + + def spy_compare_digest(provided, expected): + calls.append((provided, expected)) + return provided == expected + + monkeypatch.setattr(sophia_governor.hmac, "compare_digest", spy_compare_digest) + + denied = client.post( + "/sophia/governor/review", + headers={"X-Admin-Key": "wrong-admin"}, + json={ + "event_type": "pending_transfer", + "payload": {"amount_rtc": 50}, + }, + ) + assert denied.status_code == 401 + + accepted = client.post( + "/sophia/governor/review", + headers={"X-API-Key": "test-admin"}, + json={ + "event_type": "pending_transfer", + "source": "pytest.manual", + "payload": {"amount_rtc": 50}, + }, + ) + assert accepted.status_code == 200 + + assert calls == [ + ("wrong-admin", "test-admin"), + ("test-admin", "test-admin"), + ] + + def test_governor_endpoints_report_status_and_recent(client): review = client.post( "/sophia/governor/review",