136 lines
5.4 KiB
Python
136 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
"""CAP_SYS_PTRACE three-state contract tests (V2R2C2 P0).
|
|
|
|
Covers the capability boundary that lets the trusted candidate builder read
|
|
the peer's /proc/<pid>/{cgroup,exe,cmdline} across uids:
|
|
|
|
State A (no CAP / cross-uid read denied): fail closed ->
|
|
cgroup /proc/<pid>/cgroup PermissionError(EACCES) -> PEER_CGROUP_UNAVAILABLE
|
|
exe /proc/<pid>/exe PermissionError(EACCES) -> PEER_PROCESS_UNAVAILABLE
|
|
cmdline /proc/<pid>/cmdline PermissionError(EACCES) -> PEER_PROCESS_UNAVAILABLE
|
|
State B (minimal CAP, legitimate peer): resolved, readiness READY.
|
|
State C (CAP present, illegitimate peer): DENIED — capability never bypasses
|
|
the identity policy.
|
|
|
|
State B/C identity enforcement is exercised by the existing
|
|
test_trusted_candidate_builder.py (readiness READY + *_DENIED).
|
|
|
|
State A here is modelled with a monkeypatched PermissionError(EACCES) rather
|
|
than by guessing a high pid: a missing CAP_SYS_PTRACE / cross-uid read yields
|
|
exactly PermissionError(errno.EACCES) from the kernel, so the resolvers must
|
|
fail closed against that concrete error on each of the three /proc views.
|
|
This removes the prior assume-a-large-pid heuristic, which could not
|
|
deterministically produce EACCES.
|
|
"""
|
|
|
|
import errno
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP = ROOT / "active-runtime" / "app"
|
|
if str(APP) not in sys.path:
|
|
sys.path.insert(0, str(APP))
|
|
|
|
import aios_trusted_candidate_builder as mod # noqa: E402
|
|
from aios_trusted_candidate_builder import ( # noqa: E402
|
|
BuilderError,
|
|
_resolve_cgroups,
|
|
_resolve_process_identity,
|
|
)
|
|
|
|
|
|
def _eacces() -> PermissionError:
|
|
return PermissionError(errno.EACCES, "cross-uid /proc read denied")
|
|
|
|
|
|
def test_state_a_cgroup_read_denied_fails_closed(monkeypatch: pytest.MonkeyPatch):
|
|
# /proc/<pid>/cgroup read raises PermissionError(EACCES) -> PEER_CGROUP_UNAVAILABLE.
|
|
real_read_text = Path.read_text
|
|
|
|
def denied_cgroup(self, *a, **k):
|
|
if str(self) == "/proc/7/cgroup":
|
|
raise _eacces()
|
|
return real_read_text(self, *a, **k)
|
|
|
|
monkeypatch.setattr(Path, "read_text", denied_cgroup)
|
|
with pytest.raises(BuilderError, match="PEER_CGROUP_UNAVAILABLE"):
|
|
_resolve_cgroups(7)
|
|
|
|
|
|
def test_state_a_exe_read_denied_fails_closed(monkeypatch: pytest.MonkeyPatch):
|
|
# /proc/<pid>/exe realpath raises PermissionError(EACCES) -> PEER_PROCESS_UNAVAILABLE.
|
|
def denied_realpath(path):
|
|
if str(path) == "/proc/7/exe":
|
|
raise _eacces()
|
|
return os.path.realpath(path)
|
|
|
|
monkeypatch.setattr(os.path, "realpath", denied_realpath)
|
|
with pytest.raises(BuilderError, match="PEER_PROCESS_UNAVAILABLE"):
|
|
_resolve_process_identity(7)
|
|
|
|
|
|
def test_state_a_cmdline_read_denied_fails_closed(monkeypatch: pytest.MonkeyPatch):
|
|
# /proc/<pid>/cmdline read_bytes raises PermissionError(EACCES)
|
|
# -> PEER_PROCESS_UNAVAILABLE (cmdline branch, distinct from exe branch).
|
|
real_read_bytes = Path.read_bytes
|
|
|
|
def denied_cmdline(self, *a, **k):
|
|
if str(self) == "/proc/7/cmdline":
|
|
raise _eacces()
|
|
return real_read_bytes(self, *a, **k)
|
|
|
|
monkeypatch.setattr(Path, "read_bytes", denied_cmdline)
|
|
with pytest.raises(BuilderError, match="PEER_PROCESS_UNAVAILABLE"):
|
|
_resolve_process_identity(7)
|
|
|
|
|
|
def test_captrace_unit_is_live_baseline_with_minimal_capability_only():
|
|
unit = (
|
|
ROOT / "deploy" / "systemd" / "aios-trusted-candidate-builder.service"
|
|
).read_text(encoding="utf-8")
|
|
assert "AmbientCapabilities=CAP_SYS_PTRACE" in unit
|
|
assert "CapabilityBoundingSet=CAP_SYS_PTRACE" in unit
|
|
# Minimum-capability contract: NoNewPrivileges stays and the capability
|
|
# set is the live root baseline (User=root / Group=root), no invented
|
|
# ProtectProc / ProcSubset / credential paths / socket ownership.
|
|
assert "NoNewPrivileges=true" in unit
|
|
assert "User=root" in unit
|
|
assert "Group=root" in unit
|
|
assert "RestrictAddressFamilies=AF_UNIX" in unit
|
|
assert "ProtectProc" not in unit
|
|
assert "ProcSubset" not in unit
|
|
# No wildcard capability declaration beyond the single CAP_SYS_PTRACE.
|
|
assert "CapabilityBoundingSet=CAP_" not in unit.replace(
|
|
"CapabilityBoundingSet=CAP_SYS_PTRACE", ""
|
|
)
|
|
assert "AmbientCapabilities=CAP_" not in unit.replace(
|
|
"AmbientCapabilities=CAP_SYS_PTRACE", ""
|
|
)
|
|
# No divergent non-root -ptrace unit is tracked.
|
|
assert not (ROOT / "deploy" / "systemd" / "aios-trusted-candidate-builder-ptrace.service").exists()
|
|
|
|
|
|
def test_captrace_security_document_marks_capability_high_sensitivity():
|
|
doc = (
|
|
ROOT / "docs" / "security" / "trusted-builder-captrace.md"
|
|
).read_text(encoding="utf-8")
|
|
assert "PEER_PROCESS_UNAVAILABLE" in doc
|
|
assert "PEER_CGROUP_UNAVAILABLE" in doc
|
|
assert "READY" in doc
|
|
for denied in ("PEER_IDENTITY_DENIED", "PEER_CGROUP_DENIED", "PEER_PROCESS_DENIED"):
|
|
assert denied in doc
|
|
# Capability must never bypass identity policy.
|
|
assert "must never bypass" in doc
|
|
# The instruction requires CAP_SYS_PTRACE be described as high-sensitivity:
|
|
# the doc must frame it as carrying inspection/debug authority and must NOT
|
|
# positively claim it "grants no control authority".
|
|
assert "high-sensitivity" in doc
|
|
assert "broadens the process's observation and debugging authority" in doc
|
|
assert "grants no control authority" not in doc
|
|
assert "grants no write or control authority" not in doc
|