Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions infra/govern/govd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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":
Expand Down
81 changes: 81 additions & 0 deletions tests/test_ed25519_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
24 changes: 24 additions & 0 deletions tests/test_govd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading