Files
net-kingdom/local-identity/src/local_identity/revoke.py
tegwick e7bafd69fc feat(local-identity): Stage 4 — security hardening (NK-WP-0002-T04)
Permission enforcement on startup: enforce_permissions() checks store dir
(700), user files (600), signing key, TLS key, audit.log, revoked.json.
CLI and run_server() call it before any sensitive operation.

New modules:
  security.py  check_store(), enforce_permissions(), print_security_check()
  audit.py     log_event() — append-only TSV audit log (mode 600)
  revoke.py    revoke(jti), is_revoked(jti) — revocation list (mode 600)

New CLI commands:
  security-check          Print per-check pass/warn/fail report; exit 1 on failure
  revoke-token <jti|jwt>  Add JTI to revocation list; accepts raw JTI or full JWT

Serve integration:
  Audit log written for auth request, token issuance, and userinfo calls
  Revocation checked at /userinfo; revoked tokens return 401

Docs: security model section in LocalIdentity.md — threat model,
assumptions, non-guarantees, SELinux/AppArmor guidance, revocation usage.

138 tests passing (34 new for Stage 4).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 08:06:56 +01:00

53 lines
1.2 KiB
Python

"""
Token revocation list for local-identity OIDC serve.
Revoked JTIs (JWT IDs) are stored in ~/.local-identity/revoked.json (mode 600).
The list is append-only: JTIs are added but never removed.
Usage:
revoke(jti) Add a JTI to the revocation list.
is_revoked(jti) Return True if the JTI has been revoked.
"""
import json
import os
from pathlib import Path
from .store import _store_dir
def _revoked_path() -> Path:
return _store_dir() / "revoked.json"
def _load() -> set[str]:
path = _revoked_path()
if not path.exists():
return set()
try:
data = json.loads(path.read_text(encoding="utf-8"))
return set(data.get("revoked", []))
except (json.JSONDecodeError, OSError):
return set()
def _save(revoked: set[str]) -> None:
path = _revoked_path()
path.write_text(
json.dumps({"revoked": sorted(revoked)}, indent=2),
encoding="utf-8",
)
os.chmod(path, 0o600)
def is_revoked(jti: str) -> bool:
"""Return True if this JTI has been revoked. Defaults to False on read error."""
return jti in _load()
def revoke(jti: str) -> None:
"""Add a JTI to the revocation list."""
revoked = _load()
revoked.add(jti)
_save(revoked)