ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
fleet_transaction_auth.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Authenticate a mutating role payload against fleet and wrapper authority."""
4
5from __future__ import annotations
6
7import argparse
8import json
9import sys
10from pathlib import Path
11from unittest import mock
12
13sys.path.insert(0, str(Path(__file__).resolve().parent))
14
15import bench_lock_capability as blc
16import fleet_model as fm
17
18ROLE_PREFIXES = {
19 "dev_box": "dev_box_hil_runner_",
20 "hil_bench": "hil_bench_",
21}
22
23
24class TransactionError(ValueError):
25 """A role payload or controller capability was not fleet-authenticated."""
26
27
28def expected_payload(host_name: str, role: str) -> dict[str, object]:
29 """Return the exact fleet-derived policy values owned by one role."""
30 data = fm.load()
31 problems = fm.validate(data)
32 if problems:
33 raise TransactionError("fleet declaration is invalid: " + "; ".join(problems))
34 host = data.get("hosts", {}).get(host_name)
35 if not isinstance(host, dict) or host.get("class") != role:
36 msg = f"{host_name!r} is not the declared {role} host"
37 raise TransactionError(msg)
38 prefix = ROLE_PREFIXES[role]
39 payload = {
40 key: value
41 for key, value in fm.role_vars(data, host_name, host).items()
42 if key.startswith(prefix)
43 }
44 if not payload:
45 msg = f"fleet declaration produced no {role} policy payload"
46 raise TransactionError(msg)
47 return payload
48
49
50def authenticate(
51 host_name: str,
52 role: str,
53 raw_payload: str,
54 capability: blc.Capability,
55) -> None:
56 """Require exact fleet policy plus the live controller wrapper process."""
57 try:
58 actual = json.loads(raw_payload)
59 except json.JSONDecodeError as exc:
60 msg = "role payload is not valid JSON"
61 raise TransactionError(msg) from exc
62 expected = expected_payload(host_name, role)
63 if type(actual) is not dict or actual != expected:
64 msg = "role payload differs from the immutable fleet declaration"
65 raise TransactionError(msg)
66 try:
67 blc.authenticate(capability, fm.REPO_ROOT)
68 except blc.CapabilityError as exc:
69 msg = f"controller bench capability failed: {exc}"
70 raise TransactionError(msg) from exc
71
72
73def run_selftest() -> list[str]:
74 """Prove each role authority class and the controller capability fail closed."""
75 expected = {
76 "dev_box_hil_runner_service": "ra8-hil-runner.service",
77 "dev_box_hil_runner_root": "/opt/ra8-hil-runner",
78 "dev_box_hil_runner_url": "https://example.invalid/runner.tgz",
79 "dev_box_hil_runner_sha256": "a" * 64,
80 "dev_box_hil_runner_bench_alias": "star",
81 }
82 capability = blc.Capability("0123456789abcdef", 41, 1000, "star.local")
83 failures: list[str] = []
84 module = sys.modules[__name__]
85 with (
86 mock.patch.object(module, "expected_payload", return_value=expected),
87 mock.patch.object(blc, "authenticate", return_value=None),
88 ):
89 authenticate("dev", "dev_box", json.dumps(expected), capability)
90 for key in expected:
91 changed = dict(expected)
92 changed[key] = "attacker-controlled"
93 try:
94 authenticate("dev", "dev_box", json.dumps(changed), capability)
95 except TransactionError:
96 pass
97 else:
98 failures.append(f"role payload override escaped: {key}")
99 with (
100 mock.patch.object(module, "expected_payload", return_value=expected),
101 mock.patch.object(blc, "authenticate", side_effect=blc.CapabilityError("forged")),
102 ):
103 try:
104 authenticate("dev", "dev_box", json.dumps(expected), capability)
105 except TransactionError:
106 pass
107 else:
108 failures.append("direct role payload passed without controller capability")
109 return failures
110
111
112def main() -> int:
113 """Parse one Ansible-local authentication request."""
114 if sys.argv[1:] == ["--selftest"]:
115 failures = run_selftest()
116 for failure in failures:
117 print(f"fleet-transaction-auth selftest: {failure}", file=sys.stderr)
118 return 1 if failures else 0
119 parser = argparse.ArgumentParser(description=__doc__)
120 parser.add_argument("host")
121 parser.add_argument("role", choices=tuple(ROLE_PREFIXES))
122 parser.add_argument("payload_json")
123 parser.add_argument("lock_id")
124 parser.add_argument("holder_pid", type=int)
125 parser.add_argument("holder_start_ticks", type=int)
126 parser.add_argument("holder_target")
127 args = parser.parse_args()
128 capability = blc.Capability(
129 args.lock_id,
130 args.holder_pid,
131 args.holder_start_ticks,
132 args.holder_target,
133 )
134 try:
135 authenticate(args.host, args.role, args.payload_json, capability)
136 except (OSError, TransactionError) as exc:
137 print(f"fleet-transaction-auth: {exc}", file=sys.stderr)
138 return 3
139 print("fleet-transaction-auth: authenticated")
140 return 0
141
142
143if __name__ == "__main__":
144 raise SystemExit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298