From 3f80f5924c06df59b5948e172b862bb6db3a6306 Mon Sep 17 00:00:00 2001 From: Rui He Date: Sat, 25 Jul 2026 23:06:18 -0400 Subject: [PATCH 1/2] =?UTF-8?q?fix(identity):=20actually=20WIRE=20the=20bu?= =?UTF-8?q?ilt-in=20verifier=20=E2=80=94=20a=20config=20key=20registers=20?= =?UTF-8?q?nothing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #234 shipped the Ed25519 verifier and #232 the seam, and between them the feature was unusable: nothing ever called register_verifier. Setting auth_verifier="ed25519" made every claim fail closed — correct behaviour, and a silently dead feature. Found by standing a real govd up from merged main with auth_verifier="ed25519" and watching a valid assertion resolve to nobody. No unit test could have caught it: both halves were individually correct. serve() now calls install_builtin_verifier(cfg) before the first claim. Only a name we SHIP is installed; an unknown name installs nothing and therefore resolves nobody, which is the intended fail-closed behaviour for a typo — it must never fall back to the bearer-secret path. Verifier CODE stays in the image by design: a verifier loaded from the mounted config would be ungoverned code executing inside the syscall boundary, which is the one thing this system exists to prevent. The mount configures WHICH verifier, never its body. Verified against a real govd over real HTTP (local, merged main, ed25519 scheme): alice's assertion -> 200 allow, and the signed chain records principal=alice undeclared key -> 401 bearer secret -> 401 no credential -> 401 That `principal=alice` is the point of the whole exercise: a named identity in the audit trail, which a shared per-node token can never provide. TESTS. Two unit tests pin the helper, and — because mutation showed deleting the serve() call left every unit test GREEN — an integration test starts a real govd on a free port and authenticates over HTTP, so the CALL SITE is covered, not just the function. Re-mutated after adding it: the suite now goes red. Co-Authored-By: Claude Opus 5 (1M context) --- infra/govern/govd.py | 23 +++++++++++ tests/test_ed25519_auth.py | 81 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/infra/govern/govd.py b/infra/govern/govd.py index 21be259..7918899 100644 --- a/infra/govern/govd.py +++ b/infra/govern/govd.py @@ -119,6 +119,28 @@ def load_config(path=None): return cfg +def install_builtin_verifier(cfg): + """Register the built-in identity verifier NAMED by `auth_verifier`, if any. + + The seam accepts any registered `bearer -> subject` resolver, but a resolver has to be REGISTERED to + exist — and a config key on its own registers nothing. Without this, setting `auth_verifier: "ed25519"` + would fail closed on every claim: correct, but silently unusable. + + Only a name we ship is installed. An unknown name installs nothing and therefore resolves nobody, which + is the intended fail-closed behaviour for a typo — it must never fall back to the bearer-secret path. + Verifier CODE deliberately lives in the image, never on the mounted config: a verifier loaded from a + writable path would be ungoverned code executing inside the syscall boundary, which is the one thing + this system exists to prevent. The mount configures WHICH verifier and its parameters, never its body. + """ + name = str(cfg.get("auth_verifier") or "") + if name in ("", "token_sha"): + return None # the built-in bearer-secret path; nothing to install + if name == "ed25519": + from infra.govern import ed25519_auth + return ed25519_auth.install("ed25519") + return None # unknown -> nothing registered -> resolves nobody + + def ensure_monitor_token(cfg): """Fill the monitor-token default by the final mode: a friendly 'admin' for LOCAL use, a strong random token for REMOTE (network-exposed) so it is never guessable. Override anytime with GOVD_MONITOR_TOKEN.""" @@ -1571,6 +1593,7 @@ def _load_exec_mode(cfg, httpd): def serve(cfg): Handler.timeout = cfg.get("socket_timeout", SOCKET_TIMEOUT) ensure_monitor_token(cfg) # final mode is known here (after --mode) + install_builtin_verifier(cfg) # wire the identity scheme BEFORE the first claim require_closed_auth(cfg) # refuse a network-exposed plane with auth off store = Store(cfg["record_root"], cfg=cfg) if cfg["mode"] == "remote": diff --git a/tests/test_ed25519_auth.py b/tests/test_ed25519_auth.py index ddeebfb..2d182e0 100644 --- a/tests/test_ed25519_auth.py +++ b/tests/test_ed25519_auth.py @@ -155,3 +155,84 @@ def test_verifier_never_raises(verifier): def test_bearer_secret_does_not_authenticate_under_this_scheme(key): EA.install("t_ed4") assert P.resolve_principal("s3cret", _reg(key), "t_ed4") is None + + +# ───────────────────────── govd wiring ───────────────────────── +# A config key registers nothing on its own. Found by standing a real govd up with +# auth_verifier="ed25519" and watching a valid assertion resolve to nobody: correct +# fail-closed behaviour, and a silently unusable feature. + +def test_govd_installs_the_named_builtin_verifier(): + from infra.govern import govd + from infra.govern import principals as PP + PP._VERIFIERS.pop("ed25519", None) + assert govd.install_builtin_verifier({"auth_verifier": "ed25519"}) is not None + assert "ed25519" in PP._VERIFIERS + + +def test_govd_installs_nothing_for_the_default_or_an_unknown_name(): + from infra.govern import govd + from infra.govern import principals as PP + for name in ("", "token_sha", "oidc-typo", None): + PP._VERIFIERS.pop("oidc-typo", None) + assert govd.install_builtin_verifier({"auth_verifier": name}) is None + # an unknown name must leave NOTHING registered — it must never fall back to bearer secrets + assert "oidc-typo" not in PP._VERIFIERS + + +def test_serve_actually_wires_the_verifier_END_TO_END(tmp_path): + """Covers the CALL SITE, not just the function. + + Mutation found that deleting `install_builtin_verifier(cfg)` from serve() left every unit test green — + the helper was tested, its invocation was not. This starts a real govd with auth_verifier=ed25519 and + authenticates a real claim over HTTP, so the wiring cannot silently disappear again. + """ + import json as _json, os as _os, socket, subprocess, sys, time, urllib.error, urllib.request + from cryptography.hazmat.primitives import serialization + + repo = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))) + k = Ed25519PrivateKey.generate() + (tmp_path / "principals.json").write_text(_json.dumps({"principals": { + "alice": {"subject": EA.subject_for(EA._sign.public_raw(k)), "rate": 100, "burst": 100}}})) + (tmp_path / "govd.json").write_text(_json.dumps({ + "mode": "local", "auth_verifier": "ed25519", "record_root": str(tmp_path / "rec")})) + with socket.socket() as s: + s.bind(("127.0.0.1", 0)); port = s.getsockname()[1] + + p = subprocess.Popen([sys.executable, "-m", "infra.govern.govd", + "--config", str(tmp_path / "govd.json"), "--port", str(port)], + cwd=repo, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + env={**_os.environ, "PYTHONPATH": repo, + "GOVD_PRINCIPALS": str(tmp_path / "principals.json")}) + try: + base = f"http://127.0.0.1:{port}" + for _ in range(80): + try: + cat = _json.loads(urllib.request.urlopen(base + "/catalog", timeout=1).read()); break + except Exception: + time.sleep(0.25) + else: + p.kill(); pytest.fail("govd did not start") + + sk = next(s for s in cat["skills"] if s.get("verified") + and any(not q.get("destructive") for q in s["perks"])) + perk = next(q for q in sk["perks"] if not q.get("destructive")) + claim = _json.dumps({"skill": sk["skill"], "perk": perk["id"], + "var_keys": (perk.get("vars") or {}).get("required") or []}).encode() + + def _post(bearer): + r = urllib.request.Request(base + "/govern", data=claim, method="POST") + r.add_header("Content-Type", "application/json") + if bearer: + r.add_header("Authorization", "Bearer " + bearer) + try: + return urllib.request.urlopen(r, timeout=10).status + except urllib.error.HTTPError as e: + return e.code + + assert _post(EA.mint_assertion(k)) == 200, "a declared key must authenticate — is serve() wiring it?" + assert _post(EA.mint_assertion(Ed25519PrivateKey.generate())) == 401 # undeclared + assert _post("a-static-bearer-secret") == 401 # wrong scheme + assert _post(None) == 401 + finally: + p.kill() From 87dbc7548dc4927a31ee99d370333c67ca731e52 Mon Sep 17 00:00:00 2001 From: Rui He Date: Sat, 25 Jul 2026 23:21:13 -0400 Subject: [PATCH 2/2] test(govd): cover install_builtin_verifier in govd.py's RATCHET SLICE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's enforcement-surface mutation ratchet caught this and it was right to: [FAIL] infra/govern/govd.py: score=0.95 floor=1.0 survivors=['==->!=@9986'] MUTATION REGRESSION below floor: [('infra/govern/govd.py', 0.95, 1.0)] govd.py was at a perfect 1.0 — every mutant killed — and the new `if name == "ed25519"` branch dropped it to 0.95. The branch was NOT untested: tests/test_ed25519_auth.py kills that mutant. But the ratchet drives each enforcement-surface module against a DESIGNATED slice (infra/govern/selfmonitor_policy.json: govd.py -> tests/test_govd.py), so a proof living anywhere else is invisible to it. Coverage for code in govd.py belongs in govd.py's slice; that policy is the point, not an obstacle to route around — the alternative (widening the slice) would dilute the ratchet for every module. Verified by re-running the exact CI survivor locally: mutating `==` to `!=` now fails tests/test_govd.py::test_install_builtin_verifier_registers_only_a_name_we_ship, and the suite is green restored. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_govd.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_govd.py b/tests/test_govd.py index 2a435a9..5fc8473 100644 --- a/tests/test_govd.py +++ b/tests/test_govd.py @@ -920,3 +920,27 @@ def test_govern_propagates_cargo_only_on_allow_not_on_reject(): v = govd.govern(ledger, {}, scope={"skills": ["*"]}) assert v["decision"] == "reject" and v.get("cargo") is None assert any(p["id"] == "acl_cargo_denied" for p in v.get("problems", [])) + + +# ───────────────────────── identity scheme wiring ───────────────────────── +# install_builtin_verifier lives in govd.py, so its proof belongs in govd.py's designated ratchet slice +# (infra/govern/selfmonitor_policy.json). Covered here rather than only in test_ed25519_auth.py, where the +# mutation ratchet would never see it — the `==` branch below survived precisely because of that. + +def test_install_builtin_verifier_registers_only_a_name_we_ship(): + from infra.govern import govd as G + from infra.govern import principals as PP + + PP._VERIFIERS.pop("ed25519", None) + assert G.install_builtin_verifier({"auth_verifier": "ed25519"}) is not None + assert "ed25519" in PP._VERIFIERS + + # the default bearer-secret path installs nothing + assert G.install_builtin_verifier({"auth_verifier": ""}) is None + assert G.install_builtin_verifier({"auth_verifier": "token_sha"}) is None + assert G.install_builtin_verifier({}) is None + + # an UNKNOWN name must register nothing — a typo must never quietly install a scheme, nor fall back + PP._VERIFIERS.pop("not-a-scheme", None) + assert G.install_builtin_verifier({"auth_verifier": "not-a-scheme"}) is None + assert "not-a-scheme" not in PP._VERIFIERS