generated from coulomb/repo-seed
feat(warden): implement WARDEN-WP-0002 correctness and operational completeness
T1 — TTL max enforcement: - models.py: MAX_TTL_HOURS policy constant - ca.py: _enforce_ttl() raises CAError when spec.ttl_hours > type max - Called at top of LocalCA.sign() and VaultCA.sign() - scorecard.py: check_ttl_policy() — flags certs with issued TTL > type max - run_scorecard() now returns 5 checks T2 — Stale cert cleanup: - ca.py: _evict_cert() removes existing cert before writing new one (no accumulation) - cli.py: warden cleanup [actor] [--dry-run] command - check_no_stale_certs detail suggests 'warden cleanup' when stale certs found T3 — Outgoing signatures log: - ca.py: _append_signature_log() writes JSONL to state_dir/signatures.log - Called after every successful sign() in LocalCA and VaultCA - cli.py: warden log [actor] [--last N] [--json] command - parse_cert_metadata now also returns valid_from (needed for TTL policy check) 61 tests passing, ruff clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ from typing import List
|
||||
|
||||
from warden.ca import CAError, parse_cert_metadata
|
||||
from warden.inventory import PrincipalsInventory
|
||||
from warden.models import ACTOR_PREFIX
|
||||
from warden.models import ACTOR_PREFIX, MAX_TTL_HOURS
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -84,7 +84,45 @@ def check_no_stale_certs(state_dir: Path) -> CheckResult:
|
||||
return CheckResult(
|
||||
name="no_stale_certs",
|
||||
passed=len(stale) == 0,
|
||||
detail=f"stale certs present: {stale}" if stale else "no stale certs",
|
||||
detail=(
|
||||
f"stale certs present: {stale} — run 'warden cleanup'" if stale
|
||||
else "no stale certs"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def check_ttl_policy(state_dir: Path, inventory: PrincipalsInventory) -> CheckResult:
|
||||
"""Certs in state_dir must not exceed the type-max TTL (directive §2)."""
|
||||
if not state_dir.exists():
|
||||
return CheckResult("ttl_policy", passed=True, detail="no state dir")
|
||||
|
||||
violations = []
|
||||
for cert_path in state_dir.glob("*-cert.pub"):
|
||||
actor_name = cert_path.stem.replace("-cert", "")
|
||||
entry = inventory.actors.get(actor_name)
|
||||
if entry is None:
|
||||
continue
|
||||
try:
|
||||
meta = parse_cert_metadata(cert_path)
|
||||
except CAError:
|
||||
continue
|
||||
valid_from = meta.get("valid_from")
|
||||
if valid_from is None:
|
||||
continue
|
||||
ttl_hours = (meta["valid_before"] - valid_from).total_seconds() / 3600
|
||||
max_hours = MAX_TTL_HOURS[entry.actor_type]
|
||||
if ttl_hours > max_hours + 0.01:
|
||||
violations.append(
|
||||
f"{actor_name!r}: {ttl_hours:.1f}h issued > {max_hours}h max"
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
name="ttl_policy",
|
||||
passed=len(violations) == 0,
|
||||
detail=(
|
||||
"; ".join(violations) if violations
|
||||
else "all certs within TTL policy"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -95,4 +133,5 @@ def run_scorecard(state_dir: Path, inventory: PrincipalsInventory) -> List[Check
|
||||
check_all_actors_have_principals(inventory),
|
||||
check_no_expired_certs(state_dir),
|
||||
check_no_stale_certs(state_dir),
|
||||
check_ttl_policy(state_dir, inventory),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user