fdd87178d2
gates / gates (push) Successful in 20s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
672 lines
31 KiB
Python
672 lines
31 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for the config bundle (R-840, `11` §5.4.2): felhom-os-apply's `bundle` mode and `--install-bundle`, and
|
|
scripts/build-config-bundle.py. An in-memory host plays the files; nothing real is written or run. Each refusal has a
|
|
test; each test names the rule it pins. Red-proof: `audits/r840-config-bundle-2026-10-04/partB/redproof.txt`.
|
|
|
|
Run: python3 configs/test_felhom_config_bundle.py (also run by internal/osupdate's Go test)
|
|
"""
|
|
import sys
|
|
sys.dont_write_bytecode = True # importing the builder must not leave scripts/__pycache__ behind
|
|
import base64
|
|
import contextlib
|
|
import hashlib
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import stat as statmod
|
|
import unittest
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
REPO = HERE.parent
|
|
_loader = importlib.machinery.SourceFileLoader("osapply", os.environ.get("OSAPPLY_UNDER_TEST", str(HERE / "felhom-os-apply")))
|
|
_spec = importlib.util.spec_from_loader("osapply", _loader)
|
|
osapply = importlib.util.module_from_spec(_spec)
|
|
_loader.exec_module(osapply)
|
|
_bl = importlib.machinery.SourceFileLoader("bundlebuild", str(REPO / "scripts" / "build-config-bundle.py"))
|
|
_bs = importlib.util.spec_from_loader("bundlebuild", _bl)
|
|
builder = importlib.util.module_from_spec(_bs)
|
|
_bl.exec_module(builder)
|
|
|
|
PLAN = "/var/lib/felhom-agent/os/plan-b1.json"
|
|
BUNDLE = "/var/lib/felhom-agent/os/bundle-0.143.0.json"
|
|
HOST = "demo-hp-bb76ea"
|
|
INSTALLER = REPO.parent / "felhom.eu" / "scripts" / "felhom-host-install.sh"
|
|
|
|
|
|
class St:
|
|
def __init__(self, mode, uid):
|
|
self.st_mode, self.st_uid, self.st_size = mode, uid, 100
|
|
|
|
|
|
class Box:
|
|
"""An in-memory host. files: path -> bytes; modes/uids per path; dirs: a set."""
|
|
|
|
def __init__(self, bundle_bytes, signed, oob=False, signers=True):
|
|
self.files, self.modes, self.uids = {}, {}, {}
|
|
self.dirs = {osapply.OOB_DIR} if oob else set()
|
|
self.put(osapply.TRUST_FILE, json.dumps({"host_id": HOST, "ring0_slow_lane": False}).encode(), 0o644)
|
|
if signers:
|
|
self.put(osapply.TRUST_SIGNERS, b'felhom-op-1 namespaces="felhom-op-v1" ssh-ed25519 AAAA felhom-op-1\n', 0o644)
|
|
self.put("/proc/sys/kernel/panic", b"0\n", 0o644)
|
|
self.plan = {"release_id": "bundle-0.143.0", "layer": "host", "mode": "bundle", "bundle": BUNDLE, "signed": signed}
|
|
self.put(PLAN, json.dumps(self.plan).encode(), 0o600, uid=999)
|
|
self.put(BUNDLE, bundle_bytes, 0o600, uid=999)
|
|
self.sig_rc, self.nonces, self.clock = 0, {}, 1791115200.0 # 2026-10-04T12:00:00Z
|
|
self.calls, self.logs, self.writes = [], [], []
|
|
self.visudo_fail = False # the WHOLE sudoers (`visudo -c`) after install
|
|
self.sudo_l = " (root) NOPASSWD: /usr/local/sbin/felhom-os-apply --plan /var/lib/felhom-agent/os/plan-*.json\n"
|
|
self.guard = {"armed": True, "kernel_panic": 10}
|
|
|
|
def put(self, p, data, mode, uid=0):
|
|
self.files[p], self.modes[p], self.uids[p] = data, mode, uid
|
|
|
|
# Runner interface
|
|
def now(self):
|
|
return self.clock
|
|
|
|
def log(self, line):
|
|
self.logs.append(line)
|
|
|
|
def agent_uid(self):
|
|
return 999
|
|
|
|
def verify_sig(self, signers, key_id, ns, blob, sig):
|
|
self.verified = (signers, key_id, ns)
|
|
return self.sig_rc
|
|
|
|
def read_nonces(self):
|
|
return dict(self.nonces)
|
|
|
|
def write_nonces(self, d):
|
|
self.nonces = dict(d)
|
|
|
|
def read_file(self, p):
|
|
if p not in self.files:
|
|
raise OSError("no such file")
|
|
return self.files[p].decode()
|
|
|
|
def read_bytes(self, p):
|
|
if p not in self.files:
|
|
raise OSError("no such file")
|
|
return self.files[p]
|
|
|
|
def read_staged_once(self, p, owner_uid, limit):
|
|
if p not in self.files:
|
|
raise OSError("no such file")
|
|
if self.uids[p] != owner_uid:
|
|
raise osapply.Refused("R19", f"{p} is not a regular file owned by felhom-agent")
|
|
self.staged_reads = getattr(self, "staged_reads", 0) + 1
|
|
return self.files[p]
|
|
|
|
def stat(self, p):
|
|
if p not in self.files:
|
|
raise OSError("no such file")
|
|
return St(statmod.S_IFREG | self.modes[p], self.uids[p])
|
|
|
|
def lexists(self, p):
|
|
return p in self.files
|
|
|
|
def isdir(self, p):
|
|
return p in self.dirs
|
|
|
|
def put_file(self, p, data, mode):
|
|
self.writes.append(p)
|
|
self.put(p, data, mode)
|
|
|
|
def remove(self, p):
|
|
self.writes.append("rm " + p)
|
|
del self.files[p]
|
|
|
|
def list_dir(self, p):
|
|
return sorted({k[len(p) + 1:].split("/")[0] for k in self.files if k.startswith(p + "/")})
|
|
|
|
def rmtree(self, p):
|
|
for k in [k for k in self.files if k.startswith(p + "/")]:
|
|
del self.files[k]
|
|
|
|
def check_content(self, kind, data):
|
|
return (1, f"{kind}: syntax error") if b"BROKEN-SYNTAX" in data else (0, "")
|
|
|
|
def host(self, argv, timeout=600, stdin=None):
|
|
self.calls.append(argv)
|
|
if argv[:2] == ["visudo", "-c"]:
|
|
return (1, "", "parse error") if self.visudo_fail else (0, "ok", "")
|
|
if argv[:2] == ["sudo", "-n"]:
|
|
return 0, self.sudo_l, ""
|
|
if argv[-2:] == ["/usr/local/sbin/felhom-priv-apply", "--self-check"]:
|
|
body = self.files.get("/usr/local/sbin/felhom-priv-apply", b"")
|
|
return (0, "felhom-priv-apply ok verbs=unit\n", "") if b"VERBS" in body else (1, "", "boom")
|
|
if argv[-1] == "--self-check":
|
|
body = self.files.get("/usr/local/sbin/felhom-os-apply", b"")
|
|
return (0, "felhom-os-apply ok bundle-format=1 files=22\n", "") if b"BUNDLE_OP" in body else (1, "", "boom")
|
|
if argv[-1] == "/usr/local/sbin/felhom-selfupdate-guarded":
|
|
return 2, "", "felhom-selfupdate-guarded: usage: ...\n"
|
|
if argv[-1] == "status" and argv[0].endswith("felhom-crash-guard"):
|
|
return 0, json.dumps(self.guard), ""
|
|
if argv[:3] == ["systemctl", "enable", "--now"] and "felhom-crash-guard.service" in argv:
|
|
self.put("/proc/sys/kernel/panic", f"{self.guard['kernel_panic']}\n".encode(), 0o644)
|
|
return 0, "", ""
|
|
|
|
|
|
def signed_job(sha, version="0.143.0", host=HOST, op="agent_config_update", nonce="b1",
|
|
issued="2026-10-04T11:50:00Z", expires="2026-10-04T12:30:00Z"):
|
|
blob = json.dumps({"expires_at": expires, "issued_at": issued, "key_id": "felhom-op-1", "nonce": nonce, "op": op,
|
|
"params": {"agent_version": version, "bundle_sha256": sha},
|
|
"target": {"guest_id": "", "host_id": host}}, sort_keys=True).encode()
|
|
return {"blob_b64": base64.b64encode(blob).decode(), "sig": "-----BEGIN SSH SIGNATURE-----\nx\n-----END SSH SIGNATURE-----\n"}
|
|
|
|
|
|
def real_bundle(version="0.143.0"):
|
|
data = builder.build(version)
|
|
return data, hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def edited_bundle(edit):
|
|
"""The real bundle, with edit(files_list) applied and every sha recomputed — a SIGNED bundle with bad content."""
|
|
b = json.loads(builder.build("0.143.0"))
|
|
edit(b["files"])
|
|
for e in b["files"]:
|
|
e["sha256"] = hashlib.sha256(base64.b64decode(e["content_b64"])).hexdigest()
|
|
data = (json.dumps(b, indent=1, sort_keys=True) + "\n").encode()
|
|
return data, hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def replace_content(files, path, fn):
|
|
for e in files:
|
|
if e["path"] == path:
|
|
e["content_b64"] = base64.b64encode(fn(base64.b64decode(e["content_b64"]))).decode()
|
|
|
|
|
|
def run(box, argv=None, environ=None):
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stdout(buf):
|
|
rc = osapply.main(argv or ["felhom-os-apply", "--plan", PLAN], runner=box, environ=environ or {})
|
|
line = [l for l in buf.getvalue().splitlines() if l.startswith("OSAPPLY-REPORT ")][-1]
|
|
return rc, json.loads(line[len("OSAPPLY-REPORT "):])
|
|
|
|
|
|
def box_files(box):
|
|
return {p: box.files[p] for p in box.files if p in osapply.BUNDLE_DESTS}
|
|
|
|
|
|
class Install(unittest.TestCase):
|
|
def test_fresh_box_gets_every_file_and_a_record(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
b = rep["bundle"]
|
|
# every path but the four OOB ones (no belt on this box)
|
|
self.assertEqual(len(b["written"]), len(osapply.BUNDLE_FILES) - 4, b)
|
|
self.assertEqual(len(b["skipped"]), 4)
|
|
self.assertEqual(box.files["/etc/sudoers.d/felhom-agent"], (REPO / "configs" / "felhom-agent.sudoers").read_bytes())
|
|
self.assertEqual(box.modes["/etc/sudoers.d/felhom-agent"], 0o440)
|
|
rec = json.loads(box.files[osapply.BUNDLE_RECORD])
|
|
self.assertEqual((rec["agent_version"], rec["bundle_sha256"], rec["authority"]), ("0.143.0", sha, "signed"))
|
|
self.assertIn("b1", box.nonces, "the job is consumed")
|
|
self.assertEqual(b["self_check"]["crash_guard"], {"armed": True, "kernel_panic": 10})
|
|
self.assertIn(["systemctl", "daemon-reload"], box.calls)
|
|
|
|
def test_sudoers_is_written_after_every_wrapper(self):
|
|
"""Order: a referenced wrapper is in place before the sudoers line that allows it."""
|
|
data, sha = edited_bundle(lambda f: f.reverse()) # the bundle lists the sudoers FIRST; the wrapper must reorder
|
|
box = Box(data, signed_job(sha))
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
w = [p for p in box.writes if p in osapply.BUNDLE_DESTS]
|
|
self.assertEqual(w[-1], "/etc/sudoers.d/felhom-agent")
|
|
self.assertLess(w.index("/usr/local/sbin/felhom-os-apply"), w.index("/etc/sudoers.d/felhom-agent"))
|
|
|
|
def test_identical_box_writes_nothing(self):
|
|
"""The demo boxes' case: hand-copied files equal to the release → 0 written, all 'same'."""
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
for dest, src, mode, _, policy in osapply.BUNDLE_FILES:
|
|
if policy != "oob":
|
|
box.put(dest, (REPO / "configs" / src).read_bytes(), mode)
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
self.assertEqual(rep["bundle"]["written"], [])
|
|
self.assertEqual(rep["bundle"]["same"], len(osapply.BUNDLE_FILES) - 5) # 4 oob skipped + crash-guard.conf kept
|
|
self.assertEqual(rep["bundle"]["kept"], ["/etc/felhom/crash-guard.conf"])
|
|
|
|
def test_a_wrong_mode_is_rewritten(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.put("/etc/sudoers.d/felhom-agent", (REPO / "configs" / "felhom-agent.sudoers").read_bytes(), 0o644)
|
|
rc, rep = run(box)
|
|
self.assertIn("/etc/sudoers.d/felhom-agent", rep["bundle"]["written"])
|
|
self.assertEqual(box.modes["/etc/sudoers.d/felhom-agent"], 0o440)
|
|
|
|
def test_tuned_crash_guard_conf_is_kept(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.put("/etc/felhom/crash-guard.conf", b"LIMIT=5\n", 0o644)
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
self.assertEqual(box.files["/etc/felhom/crash-guard.conf"], b"LIMIT=5\n")
|
|
|
|
def test_oob_files_only_on_a_box_with_the_belt(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha), oob=True)
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
self.assertIn("/etc/sudoers.d/felhom-op", rep["bundle"]["written"])
|
|
self.assertEqual(rep["bundle"]["skipped"], [])
|
|
|
|
def test_previous_copies_are_kept(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.put("/usr/local/sbin/felhom-pbs-apply", b"#!/bin/bash\necho old\n", 0o755)
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
self.assertEqual(box.files[rep["bundle"]["prev_dir"] + "/usr/local/sbin/felhom-pbs-apply"], b"#!/bin/bash\necho old\n")
|
|
|
|
|
|
class Refusals(unittest.TestCase):
|
|
"""Each: refused, and NOTHING on the box changed."""
|
|
|
|
def refused(self, box, code):
|
|
before = dict(box_files(box))
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 2, rep)
|
|
self.assertEqual(rep["refused"]["code"], code, rep)
|
|
self.assertEqual(box_files(box), before, "a refusal changed a file")
|
|
self.assertNotIn(osapply.BUNDLE_RECORD, box.files)
|
|
return rep
|
|
|
|
def test_wrong_sha_is_refused(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job("0" * 64))
|
|
self.refused(box, "R18")
|
|
self.assertEqual(box.nonces, {}, "a wrong sha must not burn the job")
|
|
|
|
def test_bad_signature_is_refused(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.sig_rc = 255
|
|
self.refused(box, "R3")
|
|
|
|
def test_other_op_is_refused(self):
|
|
data, sha = real_bundle()
|
|
self.refused(Box(data, signed_job(sha, op="agent_update")), "R3")
|
|
|
|
def test_other_host_is_refused(self):
|
|
data, sha = real_bundle()
|
|
self.refused(Box(data, signed_job(sha, host="demo-felhom-8363b5")), "R3")
|
|
|
|
def test_expired_job_is_refused(self):
|
|
data, sha = real_bundle()
|
|
self.refused(Box(data, signed_job(sha, expires="2026-10-04T11:55:00Z")), "R3")
|
|
|
|
def test_replayed_job_is_refused(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.nonces = {"b1": box.clock + 600}
|
|
self.refused(box, "R3")
|
|
|
|
def test_version_mismatch_is_refused(self):
|
|
data, sha = real_bundle()
|
|
self.refused(Box(data, signed_job(sha, version="0.142.1")), "R18")
|
|
|
|
def test_sudoers_failing_visudo_is_refused(self):
|
|
data, sha = edited_bundle(lambda f: replace_content(f, "/etc/sudoers.d/felhom-agent", lambda c: c + b"BROKEN-SYNTAX\n"))
|
|
self.refused(Box(data, signed_job(sha)), "R18")
|
|
|
|
def test_sudoers_dropping_the_route_is_refused(self):
|
|
data, sha = edited_bundle(lambda f: replace_content(f, "/etc/sudoers.d/felhom-agent",
|
|
lambda c: c.replace(b"/usr/local/sbin/felhom-os-apply --plan", b"/bin/true --plan")))
|
|
self.refused(Box(data, signed_job(sha)), "R18")
|
|
|
|
def test_wrapper_without_bundle_mode_is_refused(self):
|
|
data, sha = edited_bundle(lambda f: replace_content(f, "/usr/local/sbin/felhom-os-apply",
|
|
lambda c: c.replace(b'BUNDLE_OP = "agent_config_update"', b'BUNDLE_OPX = 1')))
|
|
self.refused(Box(data, signed_job(sha)), "R18")
|
|
|
|
def test_python_syntax_error_is_refused(self):
|
|
data, sha = edited_bundle(lambda f: replace_content(f, "/usr/local/sbin/felhom-crash-guard", lambda c: c + b"\ndef (\n"))
|
|
self.refused(Box(data, signed_job(sha)), "R18")
|
|
|
|
def test_unit_with_runtime_directory_is_refused(self):
|
|
data, sha = edited_bundle(lambda f: replace_content(f, "/etc/systemd/system/felhom-mgmt-watchdog.service",
|
|
lambda c: c + b"RuntimeDirectory=sshd\n"))
|
|
self.refused(Box(data, signed_job(sha)), "R18")
|
|
|
|
def test_agent_unit_not_as_the_agent_user_is_refused(self):
|
|
data, sha = edited_bundle(lambda f: replace_content(f, "/etc/systemd/system/felhom-agent.service",
|
|
lambda c: c.replace(b"User=felhom-agent", b"User=root")))
|
|
self.refused(Box(data, signed_job(sha)), "R18")
|
|
|
|
def test_a_bundle_that_changes_a_signer_is_refused(self):
|
|
"""R17: the trust root is not a bundle's to change — not even a signed one."""
|
|
def add(f):
|
|
f.append({"path": osapply.TRUST_SIGNERS, "content_b64": base64.b64encode(b"evil-key\n").decode()})
|
|
data, sha = edited_bundle(add)
|
|
box = Box(data, signed_job(sha))
|
|
self.refused(box, "R17")
|
|
self.assertIn(b"felhom-op-1", box.files[osapply.TRUST_SIGNERS])
|
|
|
|
def test_a_path_outside_the_table_is_refused(self):
|
|
def add(f):
|
|
f.append({"path": "/etc/shadow", "content_b64": base64.b64encode(b"root::0:0\n").decode()})
|
|
data, sha = edited_bundle(add)
|
|
self.refused(Box(data, signed_job(sha)), "R16")
|
|
|
|
def test_content_not_matching_its_sha_is_refused(self):
|
|
b = json.loads(builder.build("0.143.0"))
|
|
b["files"][0]["content_b64"] = base64.b64encode(b"#!/bin/bash\nexit 0\n").decode()
|
|
data = json.dumps(b).encode()
|
|
self.refused(Box(data, signed_job(hashlib.sha256(data).hexdigest())), "R18")
|
|
|
|
def test_bundle_outside_the_plan_dir_is_refused(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.plan["bundle"] = "/tmp/bundle-0.143.0.json"
|
|
box.files[PLAN] = json.dumps(box.plan).encode()
|
|
self.refused(box, "R1")
|
|
|
|
def test_bundle_not_owned_by_the_agent_is_refused(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.uids[BUNDLE] = 0
|
|
self.refused(box, "R1")
|
|
|
|
def test_no_trust_file_is_refused(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
del box.files[osapply.TRUST_FILE]
|
|
self.refused(box, "R3")
|
|
|
|
|
|
class SelfCheckUndo(unittest.TestCase):
|
|
def assert_restored(self, box, before, rep):
|
|
self.assertTrue(rep["bundle"]["rolled_back"], rep)
|
|
self.assertEqual(box_files(box), before, "the previous files must be back, byte for byte")
|
|
self.assertNotIn(osapply.BUNDLE_RECORD, box.files)
|
|
|
|
def with_old_files(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.put("/usr/local/sbin/felhom-pbs-apply", b"#!/bin/bash\necho old\n", 0o755)
|
|
box.put("/etc/sudoers.d/felhom-agent", b"# old sudoers\n", 0o440)
|
|
return box
|
|
|
|
def test_route_missing_after_install_puts_everything_back(self):
|
|
box = self.with_old_files()
|
|
before = dict(box_files(box))
|
|
box.sudo_l = " (root) NOPASSWD: /bin/true\n"
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 3, rep)
|
|
self.assert_restored(box, before, rep)
|
|
self.assertEqual(box.calls[-1], ["visudo", "-c"], "the undo re-checks the whole sudoers")
|
|
|
|
def test_visudo_failing_after_install_puts_everything_back(self):
|
|
box = self.with_old_files()
|
|
before = dict(box_files(box))
|
|
box.visudo_fail = True
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 3, rep)
|
|
self.assert_restored(box, before, rep)
|
|
|
|
def test_crash_guard_disagreeing_with_kernel_panic_puts_everything_back(self):
|
|
box = self.with_old_files()
|
|
before = dict(box_files(box))
|
|
box.guard = {"armed": True, "kernel_panic": 10}
|
|
box.host_orig = box.host
|
|
|
|
def host(argv, timeout=600, stdin=None):
|
|
if argv[:3] == ["systemctl", "enable", "--now"]:
|
|
box.calls.append(argv)
|
|
return 0, "", "" # the unit "started" but kernel.panic stayed 0
|
|
return box.host_orig(argv, timeout, stdin)
|
|
box.host = host
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 3, rep)
|
|
self.assert_restored(box, before, rep)
|
|
|
|
|
|
class TrustBootstrap(unittest.TestCase):
|
|
def test_missing_signers_verifies_against_the_pinned_key_and_creates_it(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha), signers=False)
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
self.assertEqual(box.verified[0], osapply.PINNED_SIGNERS, "verified against the pinned key, nothing else")
|
|
self.assertTrue(rep["bundle"]["signers_created"])
|
|
self.assertEqual(box.files[osapply.TRUST_SIGNERS], osapply.pinned_signers_line().encode())
|
|
self.assertEqual(box.modes[osapply.TRUST_SIGNERS], 0o644)
|
|
|
|
def test_missing_signers_and_a_job_the_pinned_key_did_not_sign(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha), signers=False)
|
|
box.sig_rc = 255
|
|
rc, rep = run(box)
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R3"))
|
|
self.assertNotIn(osapply.TRUST_SIGNERS, box.files)
|
|
|
|
def test_present_signers_are_never_touched(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.put(osapply.TRUST_SIGNERS, b'felhom-op-2 namespaces="felhom-op-v1" ssh-ed25519 BBBB felhom-op-2\n', 0o644)
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
self.assertEqual(box.verified[0], osapply.TRUST_SIGNERS)
|
|
self.assertFalse(rep["bundle"]["signers_created"])
|
|
self.assertIn(b"felhom-op-2", box.files[osapply.TRUST_SIGNERS])
|
|
|
|
def test_agent_writable_signers_are_refused(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
box.uids[osapply.TRUST_SIGNERS] = 999
|
|
rc, rep = run(box)
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R3"))
|
|
|
|
def test_pinned_operator_key_equals_the_installers(self):
|
|
"""The bootstrap key is exactly the one felhom-host-install.sh pins (OPERATOR_KEY_OPERATIONAL_*)."""
|
|
text = INSTALLER.read_text()
|
|
kid = re.search(r'^OPERATOR_KEY_OPERATIONAL_ID="([^"]+)"', text, re.M).group(1)
|
|
line = re.search(r'^OPERATOR_KEY_OPERATIONAL_LINE="([^"]+)"', text, re.M).group(1)
|
|
self.assertEqual((osapply.PINNED_OPERATOR_KEY_ID, osapply.PINNED_OPERATOR_KEY_LINE), (kid, line))
|
|
# and the file format is the installer's printf, byte for byte
|
|
self.assertIn("printf '%s namespaces=\"felhom-op-v1\" %s\\n'", text)
|
|
|
|
|
|
class InstallerEntry(unittest.TestCase):
|
|
def test_installer_installs_without_a_signature(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, None)
|
|
box.put("/root/bundle.json", data, 0o600)
|
|
rc, rep = run(box, ["felhom-os-apply", "--install-bundle", "/root/bundle.json", "--sha256", sha])
|
|
self.assertEqual(rc, 0, rep)
|
|
self.assertEqual(json.loads(box.files[osapply.BUNDLE_RECORD])["authority"], "installer")
|
|
|
|
def test_installer_entry_checks_the_sha(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, None)
|
|
box.put("/root/bundle.json", data, 0o600)
|
|
rc, rep = run(box, ["felhom-os-apply", "--install-bundle", "/root/bundle.json", "--sha256", "1" * 64])
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R18"))
|
|
|
|
def test_installer_entry_is_refused_through_sudo(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, None)
|
|
box.put("/root/bundle.json", data, 0o600)
|
|
rc, rep = run(box, ["felhom-os-apply", "--install-bundle", "/root/bundle.json", "--sha256", sha], {"SUDO_UID": "999"})
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R1"))
|
|
self.assertNotIn(osapply.BUNDLE_RECORD, box.files)
|
|
|
|
def test_self_check_answers(self):
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stdout(buf):
|
|
rc = osapply.main(["felhom-os-apply", "--self-check"], runner=Box(b"", None))
|
|
self.assertEqual(rc, 0)
|
|
self.assertIn("bundle-format=1", buf.getvalue())
|
|
|
|
|
|
class Facts(unittest.TestCase):
|
|
def test_state_reports_drift_against_the_record(self):
|
|
data, sha = real_bundle()
|
|
box = Box(data, signed_job(sha))
|
|
run(box)
|
|
box.put("/usr/local/sbin/felhom-pbs-apply", b"#!/bin/bash\necho by hand\n", 0o755)
|
|
a = osapply.Apply(box, PLAN)
|
|
st = osapply.Bundle(a).state()
|
|
self.assertEqual(st["version"], "0.143.0")
|
|
self.assertEqual(st["drift"], ["/usr/local/sbin/felhom-pbs-apply"])
|
|
self.assertTrue(st["signers_present"])
|
|
|
|
def test_state_without_a_record_says_none(self):
|
|
box = Box(b"", None)
|
|
st = osapply.Bundle(osapply.Apply(box, PLAN)).state()
|
|
self.assertEqual(st["version"], "none")
|
|
self.assertNotIn("drift", st)
|
|
self.assertEqual(st["live"]["/etc/sudoers.d/felhom-agent"], "absent")
|
|
|
|
|
|
class Builder(unittest.TestCase):
|
|
def test_reproducible(self):
|
|
self.assertEqual(builder.build("0.143.0"), builder.build("0.143.0"))
|
|
|
|
def test_every_source_exists_and_every_dest_is_unique(self):
|
|
dests = [e[0] for e in osapply.BUNDLE_FILES]
|
|
self.assertEqual(len(dests), len(set(dests)))
|
|
for _, src, *_ in osapply.BUNDLE_FILES:
|
|
self.assertTrue((REPO / "configs" / src).is_file(), src)
|
|
|
|
def test_every_root_file_the_installer_writes_is_in_the_bundle(self):
|
|
"""One source of truth: a felhom root-owned path the installer names must be a bundle path, a trust file, or a
|
|
path the AGENT itself writes at run time (named here, with why). Scope is a regex over the WHOLE installer."""
|
|
text = INSTALLER.read_text()
|
|
found = set(re.findall(r"(/usr/local/sbin/felhom-[a-z-]+|/etc/systemd/system/felhom-[a-z.-]+|"
|
|
r"/etc/sudoers\.d/felhom-[a-z-]+|/etc/felhom-oob\.nft|/etc/tmpfiles\.d/felhom-[a-z.-]+|"
|
|
r"/etc/felhom/[a-z.-]+)", text))
|
|
agent_writes = {"/usr/local/sbin/felhom-shared-parent", "/etc/systemd/system/felhom-shared-parent.service"}
|
|
trust = {osapply.TRUST_FILE, osapply.TRUST_SIGNERS, osapply.TRUST_SIGNERS + ".tmp", osapply.BUNDLE_RECORD}
|
|
missing = sorted(p for p in found if p not in osapply.BUNDLE_DESTS and p not in agent_writes | trust)
|
|
self.assertEqual(missing, [], "the installer writes these root files, but the bundle does not carry them")
|
|
# the limits drop-in is named through $AGENT_UNIT in the installer
|
|
self.assertIn("/etc/systemd/system/felhom-agent.service.d/felhom-agent-limits.conf", osapply.BUNDLE_DESTS)
|
|
|
|
|
|
|
|
# ---------- R-861 (agent v0.146.0): the agent binary only by an operator-signed agent_update, checked as root ----------
|
|
STAGED = "/var/lib/felhom-agent/selfupdate/felhom-agent-0.146.0"
|
|
NEW_BIN = b"\x7fELF the new agent"
|
|
|
|
|
|
def update_job(sha, version="0.146.0", op="agent_update", nonce="u1", host=HOST):
|
|
blob = json.dumps({"expires_at": "2026-10-04T12:30:00Z", "issued_at": "2026-10-04T11:50:00Z", "key_id": "felhom-op-1",
|
|
"nonce": nonce, "op": op, "params": {"sha256": sha, "version": version},
|
|
"target": {"guest_id": "", "host_id": host}}, sort_keys=True).encode()
|
|
return {"blob_b64": base64.b64encode(blob).decode(), "sig": "-----BEGIN SSH SIGNATURE-----\nx\n-----END SSH SIGNATURE-----\n"}
|
|
|
|
|
|
def update_box(job, staged=STAGED, content=NEW_BIN):
|
|
box = Box(b"{}", None)
|
|
box.put(PLAN, json.dumps({"release_id": "agent-0.146.0", "layer": "host", "mode": "agent_update",
|
|
"signed": job, "staged": staged}).encode(), 0o600, uid=999)
|
|
box.put(STAGED, content, 0o755, uid=999)
|
|
return box
|
|
|
|
|
|
def wrapper_calls(box):
|
|
return [c for c in box.calls if c and c[0] == osapply.SELFUPDATE_WRAPPER and c[1:2] == ["apply"]]
|
|
|
|
|
|
class AgentUpdate(unittest.TestCase):
|
|
"""RED-PROOF: make agent_update skip verify_signed → test_a_bad_signature_never_reaches_the_wrapper fails."""
|
|
|
|
def test_signed_update_flips_and_burns_the_nonce(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha))
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 0, rep)
|
|
root_copy = osapply.SELFUPDATE_ROOT_DIR + "/felhom-agent-0.146.0"
|
|
# the wrapper gets the ROOT-OWNED copy of the bytes that were hashed — never the agent's path (review 2026-10-05)
|
|
self.assertEqual(wrapper_calls(box), [[osapply.SELFUPDATE_WRAPPER, "apply", root_copy, sha]])
|
|
self.assertIn(root_copy, box.writes)
|
|
self.assertNotIn(root_copy, box.files, "the root copy is removed after the flip")
|
|
self.assertEqual(box.staged_reads, 1, "the agent's file is read exactly once")
|
|
self.assertIn("u1", box.nonces)
|
|
self.assertEqual(rep["agent_update"]["version"], "0.146.0")
|
|
|
|
def test_a_staged_file_the_agent_does_not_own_is_refused(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha))
|
|
box.uids[STAGED] = 0
|
|
rc, rep = run(box)
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R19"), rep)
|
|
self.assertEqual(wrapper_calls(box), [])
|
|
|
|
def test_a_bad_signature_never_reaches_the_wrapper(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha))
|
|
box.sig_rc = 1
|
|
rc, rep = run(box)
|
|
self.assertTrue(rep.get("refused"), f"a job whose signature does not verify was NOT refused: {rep}")
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R3"), rep)
|
|
self.assertEqual(wrapper_calls(box), [])
|
|
|
|
def test_a_staged_binary_the_signature_does_not_pin_is_refused(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha), content=b"\x7fELF something the agent put there")
|
|
rc, rep = run(box)
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R19"), rep)
|
|
self.assertEqual(wrapper_calls(box), [])
|
|
self.assertNotIn("u1", box.nonces, "a refused job keeps its nonce (the operator fixes the file, not the key)")
|
|
|
|
def test_another_staging_path_is_refused(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha), staged="/tmp/felhom-agent-0.146.0")
|
|
rc, rep = run(box)
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R19"), rep)
|
|
self.assertEqual(wrapper_calls(box), [])
|
|
|
|
def test_a_bundle_job_is_not_an_agent_update(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha, op="agent_config_update"))
|
|
rc, rep = run(box)
|
|
self.assertEqual((rc, rep["refused"]["code"]), (2, "R3"), rep)
|
|
|
|
def test_another_hosts_job_and_a_replay_are_refused(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha, host="tester-1-d70be4"))
|
|
self.assertEqual(run(box)[1]["refused"]["code"], "R3")
|
|
box2 = update_box(update_job(sha))
|
|
box2.nonces["u1"] = 1999999999
|
|
self.assertEqual(run(box2)[1]["refused"]["code"], "R3")
|
|
self.assertEqual(wrapper_calls(box) + wrapper_calls(box2), [])
|
|
|
|
def test_a_failed_flip_keeps_the_nonce(self):
|
|
sha = hashlib.sha256(NEW_BIN).hexdigest()
|
|
box = update_box(update_job(sha))
|
|
orig = box.host
|
|
box.host = lambda argv, timeout=600, stdin=None: (1, "", "refusing apply: sha mismatch") if argv[:1] == [osapply.SELFUPDATE_WRAPPER] else orig(argv, timeout, stdin)
|
|
rc, rep = run(box)
|
|
self.assertEqual(rc, 3, rep)
|
|
self.assertNotIn("u1", box.nonces)
|
|
|
|
|
|
|
|
class SelfupdateWrapperConfinement(unittest.TestCase):
|
|
"""R-861 (v0.146.1): the A/B wrapper takes only felhom-os-apply's root-owned copy, never the agent's staging dir
|
|
(a file there can be swapped between the wrapper's sha check and its copy). The path check runs before anything
|
|
is touched, so the real script can be run here unprivileged.
|
|
RED-PROOF: point ROOT_STAGING back at /var/lib/felhom-agent/selfupdate → this fails (the path is accepted and the
|
|
script goes on to `staged file missing`)."""
|
|
|
|
def test_the_agents_staging_dir_is_refused(self):
|
|
import subprocess
|
|
sha = "0" * 64
|
|
p = subprocess.run(["sh", str(HERE / "felhom-selfupdate-guarded"), "apply",
|
|
"/var/lib/felhom-agent/selfupdate/felhom-agent-0.146.1", sha], capture_output=True, text=True)
|
|
self.assertEqual(p.returncode, 1, p.stderr)
|
|
self.assertIn("outside /var/lib/felhom-os-apply/agent-update", p.stderr)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|