Files
dashboards/tests/test_certs.py
T
ducoterra cffbac5771 phase: 15_agent_mtls
All green. Report:

**Verified / implemented (final pass of `15_agent_mtls`)**
- Wrote `data/logs/verify-15-tls-e2e.py`: a real `host_agent.py --cert-bundle` + a real dashboard process over a scratch DB (`HOST_TARGETS=loop=https://…`) — **27/27 PASS**, transcript `verify-15-tls-e2e.out`, notes `data/logs/NOTES-tls-rollout.md`.
- Read-only checks against the running dev store: 5 `certs.*` rows byte-identical across a restart; served bundle == the `meta` row (1430 B); 0 events containing `-----BEGIN`; unsigned `/certs`→`303 ?next=`, unsigned `/api/certs`→`401`, signed-in `200`; screenshots `data/logs/15-final-{certs,system}.png`.
- Docs (step 6, full review): README — added *Protecting an agent, host by host* (the rollout task-07 left out), a `tests/test_certs.py`/TLS-matrix/sandbox-installer paragraph, and fixed two stale “the one write endpoint in the app” claims (`NUT_ALLOW_ACTIONS`, Battery-test buttons). AGENTS.md — test count 1424→1427, bundle-fetch command in *Commands*, a Testing-policy bullet on structurally pinning a key-holding module, fleet transport bullet (token mode is now the compatibility path), gap 11 final state.

**Tests / lint (full suite 100% green)**
- `uv run pytest` → **1427 passed, 15 skipped**, exit 0 · `bash .agents/validate.sh` → **exit 0** (coverage **98.94%** vs 94% floor, `certs.py` 100%; ruff “All checks passed”; pyright 0 errors; `node --check` clean; hygiene clean)
- `uv run pytest -m live --live` → **11 passed / 0 failed** · `./scripts/dev-server.sh status` → all 9 sources `ok`
- `uv pip list --outdated` → empty after bumping `fastapi 0.152.0 → 0.153.0` (`uv.lock` only); full suite re-run green.

**Completion criteria**
- Fresh-DB root CA, idempotent: ✅ (tests + live store hashes) · agent TLS matrix incl. wrong-CA/certless 403/`/health` CA-only/revoked 403 + live denylist re-read: ✅
- `https` `host:`/`disks:` `ok` + revoked says “agent certificate revoked” with no dial: ✅ on loopback (real agent + real dashboard) and spy-pinned hermetically; **not** on the fleet (below)
- Signed-in `/certs` + rotate/revoke/root-rotate + byte-identical bundle download: ✅ · installer bundle/unit/`--status` over TLS: ✅ (0640, see deviation)
- `pytest`/`validate.sh`/`--outdated`: ✅ · token-mode byte-identical, engine/UPS untouched, public surface unchanged (route walk): ✅
- **Live fleet TLS (deskwork+gateway flip): NOT done** — no live changes. It needs root on hosts without the installer grant (`deskwork`), has no both-ways-working window (`http://` dashboard vs https-only agent), and would deploy the agent from this not-yet-committed tree. Exact procedure recorded in README + `data/logs/NOTES-tls-rollout.md`.

**Deviations**: bundle mode `0640 root:dashboards-agent` instead of the task file’s `0600` (the agent re-reads its own bundle for the denylist; 0600 breaks the unit — already recorded in AGENTS.md); the loopback dashboard substitutes for the fleet flip. Incidental: a stray `pkill` of mine killed the dev server mid-pass; restarted and verified healthy.

**Next pending phase**: `.agents/phases/todo/16_agent_smart_pass_budget` (then `17_installer_device_filter`).
2026-10-20 10:02:16 -04:00

1095 lines
48 KiB
Python

"""`src/dashboards/certs.py` — the dashboard's CA, its client cert, and every agent's, tested.
The lifecycle here is the part of phase 15 that cannot be checked in a browser: a root CA that
regenerates on restart silently untrusts every installed agent bundle; a bundle carrying the root
*key* turns one stolen agent install into a forged fleet; a revocation that lands on one host's
bundle but not the others is a revocation that did not happen. So this file is mostly claims about
**stored bytes**, and the store underneath them is a dict.
That is the `tests/test_auth_bootstrap.py` idiom, and it is why `certs.py` talks to a three-method
`MetaStore` protocol: the security-relevant branch has no SQLite in it, so the decisions are
asserted directly. Three tests at the end put something real underneath instead — the real
`SampleStore` for durability, a real `ssl` handshake for usability, and the real `create_app()`
lifespan for the fail-closed startup — because "it parses" is not the same claim as "an agent
accepts it".
Nothing sleeps and nothing reads the wall clock: `now` is an argument everywhere, so a 10-year root
and a 1-year leaf are asserted as numbers.
"""
from __future__ import annotations
import ast
import json
import socket
import ssl
import tempfile
import threading
from pathlib import Path
from typing import Any
import pytest
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes
from cryptography.hazmat.primitives.serialization import (
Encoding,
NoEncryption,
PrivateFormat,
PublicFormat,
load_pem_private_key,
)
from cryptography.x509.oid import NameOID
from fastapi.testclient import TestClient
from auth_contract import ADMIN_PASSWORD, FAST_PBKDF2
from dashboards import auth, certs
from dashboards.app import create_app
from dashboards.config import Settings
from dashboards.store import SampleStore
CERTS_SOURCE = Path(__file__).resolve().parents[1] / "src" / "dashboards" / "certs.py"
#: One fixed instant for every lifetime assertion. `certs.py` never reads a clock, so an expiry is
#: this number plus the documented lifetime — no sleeps, no flaky window.
NOW = 1_700_000_000.0
CLIENT_CERT_FILE = "client-cert.pem"
CLIENT_KEY_FILE = "client-key.pem"
CA_FILE = "root-ca.pem"
class FakeMeta:
"""The three-method shape `certs.py` asks for, with no database in it.
Exactly how the auth bootstrap is tested: the lifecycle is a function over a key/value store,
so the store is a dict and the assertions are about the rows.
"""
def __init__(self, rows: dict[str, str] | None = None) -> None:
self.rows: dict[str, str] = dict(rows or {})
def get_meta(self, key: str) -> str | None:
return self.rows.get(key)
def set_meta(self, key: str, value: str) -> None:
self.rows[key] = value
def delete_meta(self, key: str) -> int:
return 1 if self.rows.pop(key, None) is not None else 0
def boot(*, now: float = NOW, rows: dict[str, str] | None = None
) -> tuple[FakeMeta, certs.CertsState]:
"""A store and the state one process start produced against it."""
store = FakeMeta(rows)
return store, certs.ensure_certs(store, now=now)
def issue(store: FakeMeta, state: certs.CertsState, name: str = "deskwork",
san: list[str] | None = None) -> certs.CertsState:
return certs.issue_agent_cert(store, state, name, san or [], now=NOW)
def rotate(store: FakeMeta, state: certs.CertsState, name: str = "deskwork",
san: list[str] | None = None) -> certs.CertsState:
return certs.reissue_agent_cert(store, state, name, san or [], now=NOW)
def revoke(store: FakeMeta, state: certs.CertsState, name: str) -> certs.CertsState:
return certs.revoke(store, state, name, now=NOW)
def root_of(state: certs.CertsState) -> x509.Certificate:
return cert_of(state.root_cert)
# --- reading the artifacts back, independently of the module -------------------------------- #
def blocks(text: str) -> list[str]:
"""Every PEM block in `text`, in order.
Reimplemented with a split rather than by calling the module's parser: a test that reads a
bundle with the code that wrote it cannot notice the format changing, and task 02's agent has
to parse it with its own stdlib code anyway.
"""
out: list[str] = []
for part in text.split("-----BEGIN ")[1:]:
label, _, rest = part.partition("-----\n")
body, _, _tail = rest.partition("-----END ")
out.append(f"-----BEGIN {label}-----\n{body}-----END {label}-----\n")
return out
def cert_of(text: str) -> x509.Certificate:
return x509.load_pem_x509_certificate(text.encode("ascii"))
def key_of(text: str) -> EllipticCurvePrivateKey:
key = load_pem_private_key(text.encode("ascii"), password=None)
assert isinstance(key, EllipticCurvePrivateKey), "this module issues P-256 keys"
return key
def public_id(public_key: PublicKeyTypes) -> bytes:
"""A public key's identity as its SPKI encoding.
Compared instead of `public_numbers()` because that attribute exists per algorithm and
`certificate.public_key()` is typed as the union of all of them; the encoding is the same
claim ("same key") and needs no cast.
"""
return public_key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
def entry_of(state: certs.CertsState, name: str) -> certs.AgentCert:
"""The entry for one host, asserted present — a missing row is the failure, not a `None`."""
entry = state.agent(name)
assert entry is not None, f"no entry stored for {name}"
return entry
def bundle_pems(bundle: str) -> dict[str, str]:
"""A bundle as `{cert, key, root}` — the agent's view of the same three blocks."""
found = blocks(bundle)
certs_found = [b for b in found if b.startswith("-----BEGIN CERTIFICATE")]
keys = [b for b in found if not b.startswith("-----BEGIN CERTIFICATE")]
assert len(certs_found) == 2 and len(keys) == 1, f"unexpected bundle shape: {found}"
return {"cert": certs_found[0], "key": keys[0], "root": certs_found[1]}
def revoked_lines(bundle: str) -> list[int]:
"""The `# REVOKED` denylist, as serials, in the order the bundle carries them."""
return [int(line.removeprefix(certs.REVOKED_LINE))
for line in bundle.splitlines() if line.startswith(certs.REVOKED_LINE)]
def rev_of(bundle: str) -> int:
lines = [line for line in bundle.splitlines() if line.startswith(certs.BUNDLE_REV_LINE)]
assert len(lines) == 1, "exactly one revision line"
return int(lines[0].removeprefix(certs.BUNDLE_REV_LINE))
def san_names(cert: x509.Certificate) -> list[str]:
return [str(item.value)
for item in cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value]
def eku_of(cert: x509.Certificate) -> list[str]:
extension = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value
return [str(item.dotted_string) for item in extension]
def cn(cert: x509.Certificate) -> str:
return str(cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value)
# --- the module's own hygiene ---------------------------------------------------------------- #
def test_the_module_owns_five_meta_keys_and_names_them_all_in_the_reset() -> None:
"""The rows an operator has to be able to type, and the message that types them for them."""
assert certs.META_KEYS == ("certs.root.key", "certs.root.cert", "certs.client.key",
"certs.client.cert", "certs.agents")
for key in certs.META_KEYS:
assert key in certs.CORRUPT_STORE_MESSAGE
assert "DELETE FROM meta" in certs.CORRUPT_STORE_MESSAGE
assert "sqlite3" in certs.CORRUPT_STORE_MESSAGE
def _tree() -> ast.Module:
return ast.parse(CERTS_SOURCE.read_text())
def test_the_module_imports_nothing_from_the_package_and_nothing_logs() -> None:
"""The `auth.py` rule: standalone, testable with a dict, with no path from a key to a log line.
A private key is a credential the same way a password is, and this app turns log lines and
exception messages into things a browser can read (`AGENTS.md` invariant 6). Keeping `logging`
out of the file entirely is the version of that rule a test can actually hold.
"""
names: set[str] = set()
for node in ast.walk(_tree()):
if isinstance(node, ast.Import):
names.update(alias.name.split(".")[0] for alias in node.names)
elif isinstance(node, ast.ImportFrom):
assert node.level == 0, "no relative import: this module imports nothing in the package"
if node.module:
names.add(node.module.split(".")[0])
assert "logging" not in names
assert "cryptography" in names, "the one third-party dependency this phase adds"
def test_the_only_raise_is_the_fixed_text_refusal() -> None:
"""Every raise site is `RuntimeError(CORRUPT_STORE_MESSAGE)`: no f-string, no interpolated value.
The row that failed to parse may be a private key, and a message quoting even a prefix of it
would go into the startup output and wherever the app prints failures. Enumerating the raise
sites is what makes the *next* one somebody adds a test failure instead of a leak.
"""
raised = [node for node in ast.walk(_tree()) if isinstance(node, ast.Raise)]
assert raised, "the fail-closed refusal exists"
for node in raised:
assert isinstance(node.exc, ast.Call) and isinstance(node.exc.func, ast.Name)
assert node.exc.func.id == "RuntimeError"
assert len(node.exc.args) == 1
message = node.exc.args[0]
assert isinstance(message, ast.Name) and message.id == "CORRUPT_STORE_MESSAGE", (
"the refusal is the module-level literal, never a built string")
assert node.cause is None or isinstance(node.cause, ast.Name), "no interpolated context"
assert not any(isinstance(child, ast.JoinedStr) for child in ast.walk(node)), (
"a raise may not interpolate anything")
# ...and the literal behind that name is a literal, not something assembled at import time.
assigned = [stmt for stmt in _tree().body if isinstance(stmt, ast.Assign)
and any(isinstance(t, ast.Name) and t.id == "CORRUPT_STORE_MESSAGE"
for t in stmt.targets)]
assert len(assigned) == 1 and isinstance(assigned[0].value, ast.Constant)
assert isinstance(assigned[0].value.value, str)
# --- first boot: generate once, then never again ---------------------------------------------- #
def test_the_first_boot_persists_exactly_the_five_cert_rows() -> None:
store, state = boot()
assert sorted(store.rows) == sorted(certs.META_KEYS)
assert state.root_cert == store.rows[certs.META_ROOT_CERT]
assert state.client_key == store.rows[certs.META_CLIENT_KEY]
assert state.agents == ()
assert json.loads(store.rows[certs.META_AGENTS]) == {}, "the empty fleet is a row too"
# PEM text, not DER and not a path: `sqlite3 … SELECT value` shows a certificate.
assert store.rows[certs.META_ROOT_CERT].startswith("-----BEGIN CERTIFICATE-----")
assert store.rows[certs.META_CLIENT_KEY].startswith("-----BEGIN PRIVATE KEY-----")
def test_a_second_boot_changes_no_byte_of_any_stored_certificate() -> None:
"""The completion criterion for the task, and the one that makes a redeploy survivable.
A root CA regenerated per start would untrust every agent bundle in the fleet, so this is a
read that only fills gaps. The later `now` is part of the test: regenerating would also move
the expiry, and a state that merely *looks* current is worse than one that refuses.
"""
store, first = boot()
before = dict(store.rows)
second = certs.ensure_certs(store, now=NOW + 3_600)
assert store.rows == before, "no row moved, including the root key"
assert (second.root_cert, second.root_key) == (first.root_cert, first.root_key)
assert (second.client_cert, second.client_key) == (first.client_cert, first.client_key)
assert (second.root_serial, second.client_serial) == (first.root_serial, first.client_serial)
assert second.root_not_after == first.root_not_after
def test_a_missing_client_pair_is_generated_under_the_stored_root() -> None:
"""A crash between two `set_meta` calls leaves half a credential; the fix is the other half."""
store, first = boot()
del store.rows[certs.META_CLIENT_KEY]
del store.rows[certs.META_CLIENT_CERT]
second = certs.ensure_certs(store, now=NOW)
assert second.root_cert == first.root_cert, "the root is never regenerated to match a leaf"
assert second.client_cert != first.client_cert
cert_of(second.client_cert).verify_directly_issued_by(root_of(second))
def test_a_new_root_takes_the_client_certificate_with_it() -> None:
"""Keeping a leaf signed by a dead root would hand the process a credential nothing trusts."""
store, first = boot()
del store.rows[certs.META_ROOT_KEY]
del store.rows[certs.META_ROOT_CERT]
second = certs.ensure_certs(store, now=NOW)
assert second.root_serial != first.root_serial
assert second.client_cert != first.client_cert
cert_of(second.client_cert).verify_directly_issued_by(root_of(second))
with pytest.raises(InvalidSignature):
cert_of(first.client_cert).verify_directly_issued_by(root_of(second))
def test_a_row_of_whitespace_is_an_absent_row() -> None:
store, state = boot(rows={certs.META_ROOT_CERT: " \n "})
assert state.root_cert.startswith("-----BEGIN CERTIFICATE-----")
assert cert_of(state.root_cert).serial_number == state.root_serial
assert store.rows[certs.META_ROOT_CERT] == state.root_cert
# --- the shapes ------------------------------------------------------------------------------- #
def test_the_root_is_self_signed_and_can_only_sign() -> None:
_store, state = boot()
root = root_of(state)
assert root.subject == root.issuer
assert cn(root) == certs.ROOT_CN
assert root.extensions.get_extension_for_class(x509.BasicConstraints).value.ca is True
usage = root.extensions.get_extension_for_class(x509.KeyUsage).value
assert usage.key_cert_sign is True and usage.crl_sign is True
assert usage.digital_signature is False, "a CA that signs data is a CA being used as a leaf"
assert root.not_valid_after_utc.timestamp() == NOW + certs.ROOT_LIFETIME.total_seconds()
assert certs.ROOT_LIFETIME.days == 3650 and certs.LEAF_LIFETIME.days == 365
assert state.root_not_after == root.not_valid_after_utc.timestamp()
def test_every_certificate_carries_the_identifiers_a_strict_verifier_requires() -> None:
"""AKI and SKI are not decoration: OpenSSL refuses a chain without them in strict mode.
Python's `ssl.create_default_context()` (which is what the dashboard's own https dial builds,
and what `curl --cacert`-style clients reach for) turns `X509_STRICT` on, and a strict
verification of a leaf with no AuthorityKeyIdentifier fails with
`CERTIFICATE_VERIFY_FAILED: Missing Authority Key Identifier` — every TLS agent would read
red, with an error that names neither the extension nor the module that left it out. The
agent's own server-side check does not use strict mode, which is why the TLS tests pass
without this and why it is pinned here instead.
"""
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
entry = state.agent("deskwork")
assert entry is not None
root = root_of(state)
root_ski = root.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
assert root_ski.digest, "a CA whose leaves cannot name it cannot be chained to strictly"
for label, cert in (("client", cert_of(state.client_cert)),
("agent", cert_of(bundle_pems(entry.bundle)["cert"]))):
identifier = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
authority = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
assert authority.value.key_identifier == root_ski.digest, f"{label}: AKI must name our root"
assert not identifier.critical and not authority.critical, (
"RFC 5280: the identifiers are non-critical, so a verifier that ignores them "
"still validates the signature")
# and a strict client context accepts what that produced, which is the claim the dial makes
context = ssl.create_default_context(cadata=state.root_cert)
assert context.verify_flags & ssl.VERIFY_X509_STRICT, "this is the mode being satisfied"
def test_the_client_certificate_chains_to_the_root_and_is_a_client_identity() -> None:
_store, state = boot()
client = cert_of(state.client_cert)
assert client.verify_directly_issued_by(root_of(state)) is None
assert cn(client) == certs.CLIENT_CN
assert eku_of(client) == [x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH.dotted_string], (
"an agent verifies a client certificate against the clientAuth purpose")
assert client.extensions.get_extension_for_class(x509.BasicConstraints).value.ca is False
assert client.not_valid_after_utc.timestamp() == NOW + certs.LEAF_LIFETIME.total_seconds()
assert san_names(client) == [certs.CLIENT_CN]
assert state.client_serial == client.serial_number
assert state.client_not_after == client.not_valid_after_utc.timestamp()
def test_an_agent_certificate_chains_to_the_root_names_the_host_and_holds_the_san() -> None:
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51", "deskwork.reeselink.com"])
entry = state.agent("deskwork")
assert entry is not None
agent = cert_of(bundle_pems(entry.bundle)["cert"])
agent.verify_directly_issued_by(root_of(state))
assert cn(agent) == "deskwork"
assert san_names(agent) == ["10.1.0.51", "deskwork.reeselink.com", "deskwork"]
assert eku_of(agent) == [x509.oid.ExtendedKeyUsageOID.SERVER_AUTH.dotted_string]
assert agent.serial_number == entry.serial
assert agent.not_valid_after_utc.timestamp() == entry.not_after == (
NOW + certs.LEAF_LIFETIME.total_seconds())
# The key in the bundle is the key the certificate was issued for. An agent that loads a
# mismatched pair fails the handshake with an error about nothing it can see.
assert public_id(key_of(bundle_pems(entry.bundle)["key"]).public_key()) == public_id(agent.public_key())
def test_the_san_holds_ip_addresses_as_ips_and_names_as_dns() -> None:
"""`10.1.0.126` has to be an iPAddress SAN or hostname verification of an IP dial fails."""
store, state = boot()
state = issue(store, state, "gateway", ["10.1.1.27"])
entry = state.agent("gateway")
assert entry is not None
general = cert_of(bundle_pems(entry.bundle)["cert"]).extensions.get_extension_for_class(
x509.SubjectAlternativeName).value
assert {type(item).__name__ for item in general} == {"IPAddress", "DNSName"}
def test_the_host_name_is_always_in_the_san_and_nothing_is_listed_twice() -> None:
"""A caller that passes no address still gets a verifiable cert, and a duplicate is one name."""
store, state = boot()
state = issue(store, state, "driveripper", ["driveripper", " driveripper ", ""])
entry = state.agent("driveripper")
assert entry is not None
assert entry.san == ("driveripper",)
assert san_names(cert_of(bundle_pems(entry.bundle)["cert"])) == ["driveripper"]
# --- issue, rotate, revoke ---------------------------------------------------------------------- #
def test_a_first_issue_is_revision_one_active_and_one_row() -> None:
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
entry = state.agent("deskwork")
assert entry is not None
assert entry.status == certs.ACTIVE
assert entry.bundle_rev == 1
assert entry.serial > 0
assert entry.not_after == NOW + certs.LEAF_LIFETIME.total_seconds()
assert rev_of(entry.bundle) == 1
stored = json.loads(store.rows[certs.META_AGENTS])
assert list(stored) == ["deskwork"]
assert sorted(stored["deskwork"]) == ["bundle", "bundle_rev", "not_after", "san", "serial",
"status"]
assert stored["deskwork"]["bundle"] == entry.bundle, "the agent holds exactly what the row holds"
def test_a_reissue_bumps_the_revision_and_replaces_the_serial_and_the_bundle() -> None:
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
first = state.agent("deskwork")
assert first is not None
state = rotate(store, state, "deskwork", ["10.1.0.51"])
second = state.agent("deskwork")
assert second is not None
assert second.bundle_rev == first.bundle_rev + 1
assert second.serial != first.serial
assert second.bundle != first.bundle
assert rev_of(second.bundle) == second.bundle_rev
assert key_of(bundle_pems(second.bundle)["key"]) != key_of(bundle_pems(first.bundle)["key"]), (
"rotation replaces the key too, or the stolen one still works")
assert len(state.agents) == 1, "an issue is not an append"
def test_issue_and_reissue_are_one_operation_under_two_names() -> None:
"""Rotating a host with nothing issued yet is legal and yields revision 1 (the task's wording)."""
store, state = boot()
rotated = rotate(store, state, "deskwork", ["10.1.0.51"])
issued = issue(store, state, "deskwork", ["10.1.0.51"])
assert entry_of(rotated, "deskwork").bundle_rev == 1
assert entry_of(issued, "deskwork").bundle_rev == 1
assert entry_of(rotated, "deskwork").status == certs.ACTIVE
assert entry_of(issued, "deskwork").status == certs.ACTIVE
def test_revoking_puts_the_serial_on_every_bundles_denylist() -> None:
"""The list lives inside each bundle, so revoking one host rewrites them all — that is the point."""
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
state = issue(store, state, "gateway", ["10.1.1.27"])
gateway = state.agent("gateway")
deskwork = state.agent("deskwork")
assert gateway is not None and deskwork is not None
state = revoke(store, state, "gateway")
assert entry_of(state, "gateway").status == certs.REVOKED
assert entry_of(state, "gateway").serial == gateway.serial, "the serial stays the denylisted one"
assert revoked_lines(entry_of(state, "gateway").bundle) == [gateway.serial]
assert revoked_lines(entry_of(state, "deskwork").bundle) == [gateway.serial], (
"an agent that cannot see the revoked serial keeps accepting that client certificate")
assert entry_of(state, "deskwork").bundle_rev == deskwork.bundle_rev, (
"a peer being revoked is not a new identity for this host")
assert entry_of(state, "deskwork").status == certs.ACTIVE
assert [entry.name for entry in certs.read_agents(store)] == ["deskwork", "gateway"]
def test_revoking_a_host_with_no_certificate_still_records_the_revocation() -> None:
"""What a revoke *does* is stop the dashboard dialing; never-issued must not be a no-op."""
store, state = boot()
state = revoke(store, state, "deskwork")
entry = state.agent("deskwork")
assert entry is not None
assert entry.status == certs.REVOKED
assert entry.bundle_rev == 1
assert revoked_lines(entry.bundle) == [entry.serial]
assert certs.agent_status(store, "deskwork") == certs.REVOKED
def test_a_reissue_after_a_revoke_gives_a_serial_that_is_not_listed() -> None:
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
state = issue(store, state, "gateway", ["10.1.1.27"])
revoked_serial = entry_of(state, "gateway").serial
state = revoke(store, state, "gateway")
state = rotate(store, state, "gateway", ["10.1.1.27"])
fresh = state.agent("gateway")
assert fresh is not None and fresh.status == certs.ACTIVE
assert fresh.serial != revoked_serial
assert revoked_lines(fresh.bundle) == [], "the fix for a revoked identity is a new one"
assert revoked_lines(entry_of(state, "deskwork").bundle) == []
assert certs.agent_status(store, "gateway") == certs.ACTIVE
def test_two_revocations_are_both_listed_and_in_one_order() -> None:
store, state = boot()
for name in ("deskwork", "gateway", "driveripper"):
state = issue(store, state, name, [f"10.1.0.{len(name)}"])
state = revoke(store, state, "deskwork")
state = revoke(store, state, "driveripper")
expected = sorted([entry_of(state, "deskwork").serial, entry_of(state, "driveripper").serial])
for name in ("deskwork", "gateway", "driveripper"):
assert revoked_lines(entry_of(state, name).bundle) == expected, "one order, in every bundle"
assert entry_of(state, "gateway").status == certs.ACTIVE
def test_agent_status_names_only_what_the_operator_revoked() -> None:
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
state = revoke(store, state, "deskwork")
assert certs.agent_status(store, "deskwork") == certs.REVOKED
assert certs.agent_status(store, "gateway") == certs.ACTIVE
assert certs.agent_status(FakeMeta(), "anything") == certs.ACTIVE, (
"no row is not a revocation: the safe answer is to keep dialing")
def test_read_agents_is_a_fresh_view_of_the_table() -> None:
"""Task 03 re-reads this each poll; a cached view would delay a revocation by a restart."""
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
assert [entry.name for entry in certs.read_agents(store)] == ["deskwork"]
snapshot = state
state = revoke(store, state, "deskwork")
assert entry_of(snapshot, "deskwork").status == certs.ACTIVE, "a returned state is a snapshot"
assert certs.read_agents(store)[0].status == certs.REVOKED
def test_read_agents_sorts_by_name_so_one_order_reaches_the_page() -> None:
store, state = boot()
for name in ("gateway", "deskwork", "driveripper"):
state = issue(store, state, name)
assert [entry.name for entry in certs.read_agents(store)] == [
"deskwork", "driveripper", "gateway"]
assert [entry.name for entry in state.agents] == ["deskwork", "driveripper", "gateway"]
def test_nothing_is_issued_on_a_read() -> None:
"""A `GET` that minted a certificate would make "did anyone rotate this?" unanswerable."""
store, state = boot()
before = dict(store.rows)
for _ in range(5):
certs.ensure_certs(store, now=NOW)
certs.read_state(store)
certs.read_agents(store)
certs.agent_status(store, "deskwork")
assert store.rows == before
assert state.agents == ()
def test_read_state_is_the_stored_authority_rather_than_the_bootstrapped_one() -> None:
"""`read_state` answers "what does the table hold now?", which is what `GET /api/certs` prints.
After a root rotation the process still holds the state it started with, so a page reading that
object would show a CA the operator has just replaced. The read goes to the rows, which is the
same reason task 03 reads `agent_status` per poll instead of caching it at startup.
"""
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
stale_root = state.root_serial
rotated = certs.rotate_root(store, state, now=NOW)
fresh = certs.read_state(store)
assert (fresh.root_serial, fresh.client_serial) == (rotated.root_serial,
rotated.client_serial)
assert fresh.root_serial != stale_root, "the old authority is not what the read answers"
assert fresh.root_cert == store.rows[certs.META_ROOT_CERT], "byte for byte, as stored"
assert [entry.name for entry in fresh.agents] == [entry.name for entry in rotated.agents]
def test_read_state_refuses_a_missing_row_instead_of_minting_one() -> None:
"""The one behaviour that separates this read from `ensure_certs`, pinned from both sides.
A GET that quietly regenerated a deleted row would both write on a read and replace the CA with
one nothing in the fleet trusts — the failure `ensure_certs` exists to bootstrap at startup and
the failure a request must never cause. `ensure_certs` on the same table generates; `read_state`
refuses, and the message is the fixed reset text that names the rows.
"""
store, _state = boot()
store.rows.pop(certs.META_CLIENT_CERT)
with pytest.raises(RuntimeError) as caught:
certs.read_state(store)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
assert store.rows.get(certs.META_CLIENT_CERT) is None, "the refusal wrote nothing back"
regenerated = certs.ensure_certs(store, now=NOW) # the startup read does mint it
assert regenerated.client_cert
@pytest.mark.parametrize("key", [certs.META_ROOT_KEY, certs.META_ROOT_CERT,
certs.META_CLIENT_KEY, certs.META_CLIENT_CERT])
def test_read_state_validates_the_rows_it_reports(key: str) -> None:
"""A page listing a corrupt authority would be a calmer way to hide the outage."""
store, _state = boot()
store.rows[key] = "not a PEM document"
with pytest.raises(RuntimeError) as caught:
certs.read_state(store)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
def test_read_state_refuses_a_key_that_is_not_the_one_its_certificate_certifies() -> None:
"""A row copied over from another row is corruption with a plausible look to it.
The two halves of a pair are separate `meta` rows, so this is exactly what a hand-edit produces,
and a process that started with a mismatched pair would sign leaves with a key nothing verifies.
"""
store, _state = boot()
store.rows[certs.META_CLIENT_KEY] = store.rows[certs.META_ROOT_KEY]
with pytest.raises(RuntimeError) as caught:
certs.read_state(store)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
def test_read_state_refuses_a_leaf_the_stored_root_did_not_sign() -> None:
"""A crash between the root write and the client write is the corrupt state, not a half-state."""
store, _state = boot()
other, _other_state = boot()
store.rows[certs.META_CLIENT_CERT] = other.rows[certs.META_CLIENT_CERT]
store.rows[certs.META_CLIENT_KEY] = other.rows[certs.META_CLIENT_KEY]
with pytest.raises(RuntimeError) as caught:
certs.read_state(store)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
# --- root rotation ------------------------------------------------------------------------------ #
def test_rotating_the_root_reissues_every_agent_and_resets_the_denylist() -> None:
store, state = boot()
for name in ("deskwork", "gateway"):
state = issue(store, state, name, [f"10.1.0.{len(name)}"])
state = revoke(store, state, "gateway")
old_root = root_of(state)
old_client_serial = state.client_serial
before = {entry.name: entry for entry in state.agents}
state = certs.rotate_root(store, state, now=NOW)
new_root = root_of(state)
assert new_root.serial_number != old_root.serial_number
assert state.client_serial != old_client_serial
cert_of(state.client_cert).verify_directly_issued_by(new_root)
for name, previous in before.items():
entry = state.agent(name)
assert entry is not None
assert entry.status == certs.ACTIVE, "every identity is replaced, so nothing stays revoked"
assert entry.bundle_rev == previous.bundle_rev + 1
assert entry.serial != previous.serial
assert entry.san == previous.san, "rotation must not change the names the dashboard dials"
cert_of(bundle_pems(entry.bundle)["cert"]).verify_directly_issued_by(new_root)
assert bundle_pems(entry.bundle)["root"] == state.root_cert
assert revoked_lines(entry.bundle) == [], "the denylist belongs to one generation"
assert json.loads(store.rows[certs.META_AGENTS])["gateway"]["bundle_rev"] == (
before["gateway"].bundle_rev + 1)
def test_no_stored_value_still_carries_the_old_generation() -> None:
"""A rotated root has to be *gone*: a stale key or cert left in a row is the leak and the bug."""
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
old_root_key, old_root_cert, old_client_key = state.root_key, state.root_cert, state.client_key
state = certs.rotate_root(store, state, now=NOW)
for key, value in store.rows.items():
assert old_root_key not in value, key
assert old_root_cert not in value, key
assert old_client_key not in value, key
assert state.root_key != old_root_key
assert root_of(state).serial_number != cert_of(old_root_cert).serial_number
entry = state.agent("deskwork")
assert entry is not None and old_root_cert not in entry.bundle
def test_rotating_the_root_with_no_agents_yet_still_leaves_a_working_credential() -> None:
store, state = boot()
state = certs.rotate_root(store, state, now=NOW)
assert state.agents == ()
cert_of(state.client_cert).verify_directly_issued_by(root_of(state))
assert sorted(store.rows) == sorted(certs.META_KEYS)
# --- the bundle --------------------------------------------------------------------------------- #
def test_the_bundle_is_metadata_then_agent_cert_then_agent_key_then_root_cert() -> None:
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
entry = state.agent("deskwork")
assert entry is not None
bundle = entry.bundle
assert bundle.startswith(f"{certs.BUNDLE_REV_LINE}1\n")
assert "# REVOKED" not in bundle, "nothing is revoked on a fresh root with nothing revoked"
assert [block.split("-----")[1] for block in blocks(bundle)] == [
"BEGIN CERTIFICATE", "BEGIN PRIVATE KEY", "BEGIN CERTIFICATE"]
assert bundle_pems(bundle)["root"] == state.root_cert, "the trust anchor is the stored root"
assert bundle_pems(bundle)["cert"] == cert_of(
bundle_pems(bundle)["cert"]).public_bytes(Encoding.PEM).decode("ascii")
def test_the_bundle_carries_the_agent_key_and_never_the_root_key() -> None:
"""The TODO's "root certificate installed with each exporter" is the root *cert*.
Asserted twice on purpose: by header count (one key block, so a second private key is visible)
and by parsing that key against both keypairs, so a swap that kept the count honest still
fails here.
"""
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
entry = state.agent("deskwork")
assert entry is not None
bundle = entry.bundle
assert bundle.count("-----BEGIN PRIVATE KEY-----") == 1
assert state.root_key not in bundle
in_bundle = key_of(bundle_pems(bundle)["key"])
agent_cert = cert_of(bundle_pems(bundle)["cert"])
assert public_id(in_bundle.public_key()) == public_id(agent_cert.public_key())
assert public_id(in_bundle.public_key()) != public_id(root_of(state).public_key())
def test_a_bundle_that_is_not_the_stored_shape_is_the_fixed_text_refusal() -> None:
"""A hand-edited or truncated row cannot be re-sealed — and re-sealing is what revoking does."""
store, state = boot()
state = issue(store, state, "deskwork", ["10.1.0.51"])
payload = json.loads(store.rows[certs.META_AGENTS])
payload["deskwork"]["bundle"] = "-----BEGIN CERTIFICATE-----\nbroken\n"
store.set_meta(certs.META_AGENTS, json.dumps(payload))
tampered = certs.ensure_certs(store, now=NOW)
with pytest.raises(RuntimeError) as caught:
revoke(store, tampered, "deskwork")
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
# --- fail closed on a corrupt row ---------------------------------------------------------------- #
@pytest.mark.parametrize("key", [certs.META_ROOT_KEY, certs.META_ROOT_CERT,
certs.META_CLIENT_KEY, certs.META_CLIENT_CERT])
def test_a_stored_pem_that_will_not_parse_refuses_to_start(key: str) -> None:
store, _state = boot()
store.rows[key] = "not a PEM document"
with pytest.raises(RuntimeError) as caught:
certs.ensure_certs(store, now=NOW)
message = str(caught.value)
assert message == certs.CORRUPT_STORE_MESSAGE
for name in certs.META_KEYS:
assert name in message, "the operator is told which rows to delete"
def test_a_key_type_this_build_cannot_use_refuses_to_start() -> None:
"""Somebody hand-inserting an RSA key gets the reset message, not a stack trace at first dial."""
store, _state = boot()
rsa_pem = rsa.generate_private_key(public_exponent=65537, key_size=2048).private_bytes(
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()).decode("ascii")
store.rows[certs.META_CLIENT_KEY] = rsa_pem
with pytest.raises(RuntimeError) as caught:
certs.ensure_certs(store, now=NOW)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
def test_a_key_that_does_not_match_its_certificate_refuses_to_start() -> None:
store, _state = boot()
other = FakeMeta()
other_state = certs.ensure_certs(other, now=NOW)
store.rows[certs.META_CLIENT_KEY] = other_state.client_key
with pytest.raises(RuntimeError) as caught:
certs.ensure_certs(store, now=NOW)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
def test_a_client_certificate_the_stored_root_did_not_sign_refuses_to_start() -> None:
"""The half-finished root rotation: a new root beside an old leaf is a dashboard that reaches nothing."""
store, _state = boot()
other = FakeMeta()
other_state = certs.ensure_certs(other, now=NOW)
store.rows[certs.META_CLIENT_CERT] = other_state.client_cert
store.rows[certs.META_CLIENT_KEY] = other_state.client_key
with pytest.raises(RuntimeError) as caught:
certs.ensure_certs(store, now=NOW)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
def test_a_root_key_and_cert_from_two_generations_refuse_to_start() -> None:
store, _state = boot()
other = FakeMeta()
other_state = certs.ensure_certs(other, now=NOW)
store.rows[certs.META_ROOT_KEY] = other_state.root_key
with pytest.raises(RuntimeError) as caught:
certs.ensure_certs(store, now=NOW)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
@pytest.mark.parametrize("raw", [
"not json at all",
"[]",
'{"deskwork": "nope"}',
'{"deskwork": {"serial": 1, "not_after": 2, "status": "active", "bundle_rev": 1, '
'"bundle": "x"}}',
'{"deskwork": {"serial": 1, "not_after": 2, "status": "active", "bundle_rev": 1, '
'"bundle": "x", "san": "deskwork"}}',
'{"deskwork": {"serial": "abc", "not_after": 2, "status": "active", "bundle_rev": 1, '
'"bundle": "x", "san": []}}',
'{"deskwork": {"serial": 1, "not_after": 2, "status": "active", "bundle_rev": 1, '
'"bundle": "", "san": []}}',
])
def test_an_agent_table_that_is_not_the_stored_shape_refuses_to_start(raw: str) -> None:
"""Every one of these is what a partial write or a hand edit could leave behind."""
store, _state = boot()
store.rows[certs.META_AGENTS] = raw
with pytest.raises(RuntimeError) as caught:
certs.ensure_certs(store, now=NOW)
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
def test_the_refusal_quotes_nothing_from_the_database() -> None:
"""invariant 6, against the value that matters most in this module: the root key."""
store, _state = boot()
secret = store.rows[certs.META_ROOT_KEY]
store.rows[certs.META_ROOT_CERT] = "truncated-----"
with pytest.raises(RuntimeError) as caught:
certs.ensure_certs(store, now=NOW)
message = str(caught.value)
body = "\n".join(secret.splitlines()[1:-1])
assert body not in message
assert body[:32] not in message
assert store.rows[certs.META_ROOT_KEY] == secret, "refusing must not rewrite the rows"
# --- durability through the real store -------------------------------------------------------------- #
def test_the_certificates_survive_a_reopen_of_the_real_database(tmp_path: Path) -> None:
"""The whole reason this lives in SQLite: a restart re-reads it and an agent install stays valid."""
path = tmp_path / "dashboards.sqlite3"
first_store = SampleStore(path)
try:
first = certs.ensure_certs(first_store, now=NOW)
issued = certs.issue_agent_cert(first_store, first, "deskwork", ["10.1.0.51"], now=NOW)
finally:
first_store.close()
reopened = SampleStore(path)
try:
second = certs.ensure_certs(reopened, now=NOW + 10)
finally:
reopened.close()
assert second.root_cert == first.root_cert
assert second.root_key == first.root_key
assert second.client_cert == first.client_cert
entry = second.agent("deskwork")
assert entry is not None
assert entry.bundle == entry_of(issued, "deskwork").bundle
assert entry.bundle_rev == 1
# --- on-disk material ------------------------------------------------------------------------------ #
def test_the_material_is_three_private_files_and_never_the_root_key(tmp_path: Path) -> None:
_store, state = boot()
material = certs.write_material(state, tmp_path)
try:
assert sorted(p.name for p in material.dir.iterdir()) == [
CLIENT_CERT_FILE, CLIENT_KEY_FILE, CA_FILE]
assert material.ca.read_text() == state.root_cert
assert material.client_cert.read_text() == state.client_cert
assert material.client_key.read_text() == state.client_key
assert (material.dir.stat().st_mode & 0o777) == 0o700
for path in (material.ca, material.client_cert, material.client_key):
assert (path.stat().st_mode & 0o777) == 0o600, str(path)
assert state.root_key not in "\n".join(p.read_text() for p in material.dir.iterdir())
assert material.state is state
finally:
certs.remove_material(material)
def test_the_material_defaults_to_the_environment_tmp_dir() -> None:
_store, state = boot()
material = certs.write_material(state)
try:
assert material.dir.name.startswith("dashboards-certs-")
assert material.dir.parent.resolve() == Path(tempfile.gettempdir()).resolve()
assert material.ca.exists()
finally:
certs.remove_material(material)
def test_removing_the_material_takes_the_directory_and_tolerates_none(tmp_path: Path) -> None:
_store, state = boot()
material = certs.write_material(state, tmp_path)
certs.remove_material(material)
assert not material.dir.exists()
certs.remove_material(None)
# --- the material works in a real TLS handshake ----------------------------------------------------- #
def test_the_material_and_a_bundle_complete_a_real_client_certificate_handshake(
tmp_path: Path) -> None:
"""The claim tasks 02 and 03 rest on, checked with `ssl` instead of with this module's parser.
A certificate `cryptography` wrote that `ssl` will not load is the worst bug available here:
every other test in this file would stay green while no agent in the fleet could handshake.
The server side runs with `CERT_OPTIONAL`, which is task 02's assumption that `/health` stays
probeable without a client certificate, so both answers are asserted: a peer certificate for
the dashboard, and none for a bare probe.
"""
# Real clock for this one test: `ssl` checks the validity window itself, so the fixed `NOW`
# this file mints everything else at would be "certificate has expired" by 2027.
store = FakeMeta()
state = certs.ensure_certs(store)
state = certs.issue_agent_cert(store, state, "localhost", ["127.0.0.1", "localhost"])
entry = state.agent("localhost")
assert entry is not None
pems = bundle_pems(entry.bundle)
(tmp_path / "agent-cert.pem").write_text(pems["cert"])
(tmp_path / "agent-key.pem").write_text(pems["key"])
(tmp_path / CA_FILE).write_text(state.root_cert)
base = tmp_path / "material"
base.mkdir()
material = certs.write_material(state, base)
try:
(tmp_path / CLIENT_CERT_FILE).write_text(material.client_cert.read_text())
(tmp_path / CLIENT_KEY_FILE).write_text(material.client_key.read_text())
server = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server.load_cert_chain(str(tmp_path / "agent-cert.pem"), str(tmp_path / "agent-key.pem"))
server.load_verify_locations(str(tmp_path / CA_FILE))
server.verify_mode = ssl.CERT_OPTIONAL
with_cert = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
with_cert.load_verify_locations(str(tmp_path / CA_FILE))
with_cert.load_cert_chain(str(tmp_path / CLIENT_CERT_FILE), str(tmp_path / CLIENT_KEY_FILE))
without_cert = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
without_cert.load_verify_locations(str(tmp_path / CA_FILE))
seen = _handshake(server, [with_cert, without_cert], tmp_path)
finally:
certs.remove_material(material)
assert seen[0], "the dashboard's client certificate reached the agent"
assert any(name == "commonName" and value == certs.CLIENT_CN
for name, value in seen[0]["subject"][0])
# `getpeercert()` says "no certificate" as `{}` on some builds and `None` on others; the
# handler's check (task 02) is `if not cert`, which is what this pins rather than the empty
# value's exact shape.
assert not seen[1], "a probe with no client certificate still handshakes: the /health path"
def _handshake(server: ssl.SSLContext, clients: list[ssl.SSLContext],
tmp_path: Path) -> list[Any]:
"""Serve `server` on a loopback port and run one handshake per client context."""
assert len(clients) == 2
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen(2)
listener.settimeout(15)
port = listener.getsockname()[1]
seen: list[Any] = []
def serve() -> None:
for _ in clients:
try:
raw, _peer = listener.accept()
except OSError:
return
try:
with server.wrap_socket(raw, server_side=True) as tls:
seen.append(tls.getpeercert())
tls.sendall(b"ok")
except OSError as exc: # a refused handshake is itself an answer under test
seen.append(exc)
thread = threading.Thread(target=serve, daemon=True)
thread.start()
try:
for context in clients:
with socket.create_connection(("127.0.0.1", port), timeout=15) as raw, \
context.wrap_socket(raw, server_hostname="localhost") as tls:
assert tls.recv(2) == b"ok"
finally:
listener.close()
thread.join(timeout=15)
return seen
# --- the app wiring (work item 3) ---------------------------------------------------------------- #
def settings_for(db: str) -> Settings:
return Settings(db_path=db, admin_password=ADMIN_PASSWORD, pbkdf2_iterations=FAST_PBKDF2)
def test_the_lifespan_builds_the_material_before_the_loops_start(tmp_path: Path) -> None:
"""`ctx.certs` is there for task 03 to dial with, in the injected-store branch too."""
db = str(tmp_path / "app.sqlite3")
store = SampleStore(db)
app = create_app(settings_for(db), store)
try:
with TestClient(app):
context = app.state.ctx
assert context.certs is not None
assert context.certs.ca.exists() and context.certs.client_cert.exists()
assert context.certs.client_key.exists()
directory = context.certs.dir
for key in certs.META_KEYS:
assert store.get_meta(key), f"{key} is durable, not only in memory"
assert context.auth is not None
assert auth.verify_password(ADMIN_PASSWORD, context.auth.verifier) is True
finally:
store.close()
assert not directory.exists(), "the private directory goes with the process"
def test_a_corrupt_stored_certificate_makes_the_server_refuse_to_start(tmp_path: Path) -> None:
"""Fail closed, before the loops: a dashboard with no credential cannot report anything truthfully."""
db = str(tmp_path / "corrupt.sqlite3")
store = SampleStore(db)
certs.ensure_certs(store, now=NOW)
store.set_meta(certs.META_ROOT_CERT, "the certificate that is not one")
app = create_app(settings_for(db), store)
try:
with pytest.raises(RuntimeError) as caught, TestClient(app):
pytest.fail("the lifespan yielded with unreadable certificates")
assert str(caught.value) == certs.CORRUPT_STORE_MESSAGE
assert not hasattr(app.state, "ctx"), "nothing is published, so nothing can serve"
finally:
store.close()