generated from coulomb/repo-seed
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>
This commit is contained in:
@@ -34,9 +34,12 @@ import urllib.parse
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
|
||||
from . import audit
|
||||
from . import revoke as revoke_mod
|
||||
from . import store
|
||||
from .jwt_utils import JWTError, create_token, verify_token
|
||||
from .keys import ensure_signing_key, jwk_public, key_id
|
||||
from .security import enforce_permissions
|
||||
from .tls import ensure_tls_cert
|
||||
|
||||
_BIND_HOST = "127.0.0.1"
|
||||
@@ -270,6 +273,7 @@ class OIDCHandler(http.server.BaseHTTPRequestHandler):
|
||||
"nonce": nonce or None,
|
||||
"expires_at": now + _CODE_TTL,
|
||||
}
|
||||
audit.log_event("serve/auth", username, "auth_request")
|
||||
|
||||
sep = "&" if "?" in redirect_uri else "?"
|
||||
location = (
|
||||
@@ -335,6 +339,7 @@ class OIDCHandler(http.server.BaseHTTPRequestHandler):
|
||||
nonce=code_data.get("nonce"),
|
||||
)
|
||||
|
||||
audit.log_event("serve/token", user.username, "token_issued")
|
||||
self._send_json({
|
||||
"access_token": token,
|
||||
"id_token": token,
|
||||
@@ -358,14 +363,26 @@ class OIDCHandler(http.server.BaseHTTPRequestHandler):
|
||||
try:
|
||||
payload = verify_token(token, self._private_key.public_key())
|
||||
except JWTError as exc:
|
||||
audit.log_event("serve/userinfo", None, f"invalid_token: {exc}")
|
||||
self._send_json(
|
||||
{"error": "invalid_token", "error_description": str(exc)},
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
)
|
||||
return
|
||||
|
||||
jti = payload.get("jti", "")
|
||||
sub = payload.get("sub")
|
||||
if jti and revoke_mod.is_revoked(jti):
|
||||
audit.log_event("serve/userinfo", sub, "revoked_token")
|
||||
self._send_json(
|
||||
{"error": "invalid_token", "error_description": "token has been revoked"},
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
)
|
||||
return
|
||||
|
||||
audit.log_event("serve/userinfo", sub, "ok")
|
||||
self._send_json({
|
||||
"sub": payload["sub"],
|
||||
"sub": sub,
|
||||
"email": payload.get("email"),
|
||||
"name": payload.get("name"),
|
||||
"preferred_username": payload.get("preferred_username"),
|
||||
@@ -435,6 +452,7 @@ def run_server(port: int = 8443, token_ttl: int = 3600) -> None:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
enforce_permissions()
|
||||
private_key = ensure_signing_key()
|
||||
cert_path, key_path = ensure_tls_cert()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user