3"""Authenticate the controller-side process capability for a bench hold."""
5from __future__
import annotations
12from collections.abc
import Mapping
13from dataclasses
import dataclass
14from pathlib
import Path
23 "ServerAliveInterval=15",
25 "ServerAliveCountMax=4",
27LOCK_ID_RE = re.compile(
r"[0-9a-f]{16}")
28PID_RE = re.compile(
r"[1-9][0-9]*")
32@dataclass(frozen=True)
34 """Inherited facts which must bind to the live wrapper process tree."""
38 holder_start_ticks: int
42@dataclass(frozen=True)
43class _SyntheticProcess:
44 """One synthetic proc identity used by the offline attacks."""
51 cwd: Path |
None =
None
54@dataclass(frozen=True)
56 """Controller process-tree authority for the offline attacks."""
60 capability: Capability
64class CapabilityError(ValueError):
65 """Inherited capability facts did not match the trusted wrapper."""
68def _stat_fields(proc_root: Path, pid: int) -> tuple[int, int]:
69 """Return parent PID and start ticks from one proc stat record."""
71 raw = (proc_root / str(pid) /
"stat").read_text(encoding=
"ascii")
72 tail = raw[raw.rindex(
")") + 2 :].split()
73 return int(tail[1]), int(tail[19])
74 except (OSError, ValueError, IndexError)
as exc:
75 msg = f
"cannot authenticate controller PID {pid}"
76 raise CapabilityError(msg)
from exc
79def _argv(proc_root: Path, pid: int) -> list[str]:
80 """Read one process argv as strict UTF-8 fields."""
82 raw = (proc_root / str(pid) /
"cmdline").read_bytes()
83 return [field.decode(
"utf-8",
"strict")
for field
in raw.split(b
"\0")
if field]
84 except (OSError, UnicodeError)
as exc:
85 msg = f
"cannot authenticate controller argv for PID {pid}"
86 raise CapabilityError(msg)
from exc
89def _executable(proc_root: Path, pid: int) -> Path:
90 """Resolve one process executable or fail closed."""
92 return (proc_root / str(pid) /
"exe").resolve(strict=
True)
93 except OSError
as exc:
94 msg = f
"cannot authenticate controller executable for PID {pid}"
95 raise CapabilityError(msg)
from exc
98def _ancestors(proc_root: Path, pid: int) -> list[int]:
99 """Return the bounded live ancestry for one process."""
100 result: list[int] = []
101 seen: set[int] = set()
102 while pid > 1
and pid
not in seen
and len(result) < MAX_ANCESTORS:
105 pid, _ticks = _stat_fields(proc_root, pid)
109def _decoded_fields(remote: str) -> list[str]:
110 """Decode bounded base64 arguments embedded in the fixed remote command."""
111 decoded: list[str] = []
112 for candidate
in re.findall(
r"'([A-Za-z0-9+/=]{16,})'", remote):
113 value = _decode_field(candidate)
114 if value
is not None:
115 decoded.append(value)
119def _decode_field(candidate: str) -> str |
None:
120 """Decode one bounded field without placing exception handling in the scan loop."""
122 raw = base64.b64decode(candidate, validate=
True)
123 return raw.decode(
"utf-8",
"strict")
124 except (binascii.Error, UnicodeError):
130 capability: Capability,
131 bench_host_source: bytes,
133 """Require exact ssh transport, target, reviewed source, and lock fields."""
134 argv = _argv(proc_root, capability.holder_pid)
135 if _executable(proc_root, capability.holder_pid) != Path(
"/usr/bin/ssh"):
136 msg =
"bench holder is not the fixed ssh executable"
137 raise CapabilityError(msg)
138 prefix_end = 1 + len(SSH_PREFIX)
139 if tuple(argv[1:prefix_end]) != SSH_PREFIX
or len(argv) != prefix_end + 2:
140 msg =
"bench holder ssh options are not exact"
141 raise CapabilityError(msg)
142 if argv[prefix_end] != capability.target:
143 msg =
"bench holder ssh target does not match the declared target"
144 raise CapabilityError(msg)
145 remote = argv[prefix_end + 1]
146 encoded_source = base64.b64encode(bench_host_source).decode(
"ascii")
147 if encoded_source
not in remote
or "'hold' 'wrapped'" not in remote:
148 msg =
"bench holder remote command is not the reviewed wrapped hold"
149 raise CapabilityError(msg)
150 fields = _decoded_fields(remote)
151 expected = f
"lock_id={capability.lock_id}\n"
152 if not any(expected
in value
and "hold_kind=wrapped\n" in value
for value
in fields):
153 msg =
"bench holder remote command does not bind this lock ID"
154 raise CapabilityError(msg)
155 parent, ticks = _stat_fields(proc_root, capability.holder_pid)
156 if ticks != capability.holder_start_ticks:
157 msg =
"bench holder PID was reused"
158 raise CapabilityError(msg)
162def _wrapper_parent(proc_root: Path, parent: int, repo_root: Path, current_pid: int) ->
None:
163 """Require the holder and current command beneath one reviewed bench.sh."""
164 if parent
not in _ancestors(proc_root, current_pid):
165 msg =
"bench holder is not a sibling in this wrapper transaction"
166 raise CapabilityError(msg)
167 if _executable(proc_root, parent).name !=
"bash":
168 msg =
"bench wrapper parent is not Bash"
169 raise CapabilityError(msg)
170 argv = _argv(proc_root, parent)
172 cwd = (proc_root / str(parent) /
"cwd").resolve(strict=
True)
173 except OSError
as exc:
174 msg =
"cannot authenticate bench wrapper working directory"
175 raise CapabilityError(msg)
from exc
176 expected = (repo_root /
"scripts/hil/bench.sh").resolve(strict=
True)
178 (cwd / argument).resolve()
for argument
in argv[1:]
if argument.endswith(
"bench.sh")
180 if candidates != [expected]
or "run" not in argv:
181 msg =
"controller ancestry does not contain the reviewed bench wrapper"
182 raise CapabilityError(msg)
186 capability: Capability,
188 proc_root: Path = Path(
"/proc"),
189 current_pid: int |
None =
None,
191 """Authenticate one inherited hold against the exact local process tree."""
192 if LOCK_ID_RE.fullmatch(capability.lock_id)
is None or capability.holder_pid <= 1:
193 msg =
"bench capability identity is malformed"
194 raise CapabilityError(msg)
196 capability.holder_start_ticks <= 0
197 or not capability.target
198 or any(character.isspace()
for character
in capability.target)
200 msg =
"bench capability transport is malformed"
201 raise CapabilityError(msg)
202 source = (repo_root /
"scripts/hil/lib/bench_host.sh").read_bytes()
204 msg =
"bench holder source is empty"
205 raise CapabilityError(msg)
206 parent = _holder_command(proc_root, capability, source)
207 _wrapper_parent(proc_root, parent, repo_root, current_pid
or os.getpid())
210def from_environment(environment: Mapping[str, str]) -> Capability:
211 """Parse the exact inherited capability fields without accepting aliases."""
212 pid = environment.get(
"RA8_BENCH_HOLDER_PID",
"")
213 ticks = environment.get(
"RA8_BENCH_HOLDER_START_TICKS",
"")
214 if PID_RE.fullmatch(pid)
is None or PID_RE.fullmatch(ticks)
is None:
215 msg =
"bench holder process identity is absent or malformed"
216 raise CapabilityError(msg)
218 environment.get(
"RA8_BENCH_LOCK_ID",
""),
221 environment.get(
"RA8_BENCH_HOLDER_TARGET",
""),
227 spec: _SyntheticProcess,
229 """Create one synthetic proc entry for the offline capability attacks."""
230 process = proc_root / str(spec.pid)
231 process.mkdir(parents=
True)
232 tail = [
"S", str(spec.parent), *([
"0"] * 17), str(spec.ticks)]
233 (process /
"stat").write_text(
234 f
"{spec.pid} (fixture) " +
" ".join(tail) +
"\n", encoding=
"ascii"
236 (process /
"cmdline").write_bytes(b
"\0".join(item.encode()
for item
in spec.argv) + b
"\0")
237 (process /
"exe").symlink_to(spec.executable)
238 if spec.cwd
is not None:
239 (process /
"cwd").symlink_to(spec.cwd)
242def _fixture_remote(source: bytes, lock_id: str) -> str:
243 """Render the bounded fields consumed from the reviewed holder command."""
244 encoded_source = base64.b64encode(source).decode(
"ascii")
245 fields = base64.b64encode(
246 f
"resource=bench\nlock_id={lock_id}\nhold_kind=wrapped\n".encode()
248 return f
"printf %s '{encoded_source}' | base64 -d; 'hold' 'wrapped' '{fields}'"
251def _make_fixture(base: Path) -> _Fixture:
252 """Create one wrapper, exact holder, foreign sibling, and current child."""
255 host = repo /
"scripts/hil/lib/bench_host.sh"
256 wrapper = repo /
"scripts/hil/bench.sh"
257 host.parent.mkdir(parents=
True)
258 host.write_bytes(b
"#!/bin/bash\n# reviewed\n")
259 wrapper.write_bytes(b
"#!/bin/bash\n")
260 wrapper_pid, holder_pid, foreign_pid, current_pid = 100, 101, 103, 102
263 _SyntheticProcess(wrapper_pid, 1, 500,
"/bin/bash", [
"bash", str(wrapper),
"run"], repo),
265 capability = Capability(
"0123456789abcdef", holder_pid, 600,
"star.local")
270 _fixture_remote(host.read_bytes(), capability.lock_id),
273 proc, _SyntheticProcess(holder_pid, wrapper_pid, 600,
"/usr/bin/ssh", holder_argv)
275 foreign_argv = [
"/usr/bin/ssh", *SSH_PREFIX,
"foreign.local",
"unreviewed"]
277 proc, _SyntheticProcess(foreign_pid, wrapper_pid, 601,
"/usr/bin/ssh", foreign_argv)
280 proc, _SyntheticProcess(current_pid, wrapper_pid, 700,
"/usr/bin/python3", [
"python3"])
282 return _Fixture(repo, proc, capability, current_pid)
287 capability: Capability,
291 """Require a synthetic controller capability attack to fail closed."""
293 authenticate(capability, fixture.repo, fixture.proc, fixture.current_pid)
294 except CapabilityError:
296 failures.append(f
"{label} escaped controller capability authentication")
299def run_selftest() -> list[str]:
300 """Exercise PID, ancestry, target, source, and sibling binding offline."""
301 failures: list[str] = []
302 with tempfile.TemporaryDirectory(prefix=
"ra8-capability-")
as raw:
303 fixture = _make_fixture(Path(raw))
304 capability = fixture.capability
305 authenticate(capability, fixture.repo, fixture.proc, fixture.current_pid)
306 reused = Capability(capability.lock_id, capability.holder_pid, 601, capability.target)
307 _expect_refusal(fixture, reused,
"PID reuse", failures)
308 wrong_target = Capability(capability.lock_id, capability.holder_pid, 600,
"other.local")
309 _expect_refusal(fixture, wrong_target,
"remote target drift", failures)
310 foreign = Capability(capability.lock_id, 103, 601,
"foreign.local")
311 _expect_refusal(fixture, foreign,
"foreign same-parent ssh", failures)
312 (fixture.repo /
"scripts/hil/lib/bench_host.sh").write_bytes(b
"tampered\n")
313 _expect_refusal(fixture, capability,
"reviewed holder tamper", failures)
314 (fixture.repo /
"scripts/hil/lib/bench_host.sh").write_bytes(b
"#!/bin/bash\n# reviewed\n")
318 _SyntheticProcess(detached_pid, 1, 800,
"/usr/bin/python3", [
"python3"]),
320 detached = _Fixture(fixture.repo, fixture.proc, capability, detached_pid)
321 _expect_refusal(detached, capability,
"foreign process tree", failures)