fix(auth): authenticate producer-signed decisions before authorization
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 07:51:44 +02:00
parent 6bd7129eac
commit c80a944105
30 changed files with 2059 additions and 32 deletions

View file

@ -19,6 +19,7 @@ from urllib.error import HTTPError, URLError
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from secrets_engine.errors import DecisionError
from secrets_engine.decision_signature import strict_json, load_trust, authenticate_response
from secrets_engine.openbao import read_strict_token_file
_MAX_BODY = 512 * 1024
@ -95,6 +96,9 @@ def check_decision(
base_url: str,
token_file: Path,
request: dict[str, Any],
trust_file: Path | None = None,
verifier: Path | None = None,
allow_unsigned_test_fixture: bool = False,
timeout_seconds: float = 3,
opener: Callable[..., Any] | None = None,
) -> dict[str, Any]:
@ -103,9 +107,13 @@ def check_decision(
Silence is never permission: an unreachable PDP raises rather than
returning a permissive default.
"""
require_signature = not allow_unsigned_test_fixture
if not base_url or not base_url.startswith(("http://", "https://")):
raise DecisionError("access-engine check URL is missing or invalid")
require_supported_pdp_address(base_url)
trust = load_trust(trust_file) if require_signature else None
if require_signature and (verifier is None or not Path(verifier).is_absolute() or not Path(verifier).is_file()):
raise DecisionError("access-engine signature verifier is not installed")
token = read_strict_token_file(Path(token_file), purpose="access-engine credential")
encoded = json.dumps(request).encode("utf-8")
http_request = Request(
@ -123,13 +131,15 @@ def check_decision(
body = response.read(_MAX_BODY + 1)
if len(body) > _MAX_BODY:
raise DecisionError("access-engine check body is too large")
payload = json.loads(body.decode("utf-8"))
payload = strict_json(body.decode("utf-8"))
except HTTPError as e:
raise DecisionError(f"access-engine check refused: {_status_message(e.code)}") from e
except URLError as e:
raise DecisionError("access-engine is unreachable for check") from e
except (json.JSONDecodeError, UnicodeDecodeError) as e:
except (ValueError, UnicodeDecodeError, RecursionError) as e:
raise DecisionError("access-engine check returned a non-JSON body") from e
if not isinstance(payload, dict):
raise DecisionError("access-engine check returned a non-object body")
if require_signature:
return authenticate_response(body, trust=trust, verifier=verifier)
return payload