trading-system-docs/tolaria/c5-sync-service/approval_verifier.py
Red Queen 9c8d239ae5 AUTH.3D: Human DELETE Approval Authenticity (Ed25519, verify-only)
- approval_payload: kanonischer, deterministischer Approval-Payload
- approval_signature: Ed25519 sign/verify (Christian=Private Key, Executor=Public Key only)
- approval_verifier: verify-only, alle Bindings fail-closed
- approval_state: Lifecycle CREATED->CONSUMED, Single-Use, Reservation, OUTCOME_UNKNOWN
- test_approval_auth3d: T1-T30 + adversarial (50 Tests)
- test_approval_helpers: synthetische Test-Keypairs
- sensitivity_proof: Mutationen A-J machen Tests ROT

Nur synthetische Test-Keypairs. Kein produktives Deployment.
DELETE_OPERATION_ACTIVATION bleibt BLOCKED bis AUTH.3D geprueft.
2026-08-27 09:36:21 +00:00

232 lines
8.6 KiB
Python

"""
AUTH.3D — approval_verifier.py
Verification-Logik für Human DELETE Approvals (alle §8-Checks).
Der DELETE Executor MUSS VOR DELETE alle Checks bestehen, sonst DELETE_DENIED.
FAIL CLOSED: jede Abweichung -> DELETE_DENIED. KEIN Tolaria HTTP DELETE.
"""
from __future__ import annotations
import datetime
from typing import Any, Dict, List, Optional
from approval_payload import (
APPROVAL_VERSION,
OPERATION_DELETE,
ApprovalPayloadError,
canonicalize,
parse_payload,
)
from approval_signature import key_id, verify
# Reason Codes (FAIL CLOSED)
RC_SCHEMA_INVALID = "DELETE_DENIED_SCHEMA_INVALID"
RC_VERSION_UNSUPPORTED = "DELETE_DENIED_VERSION_UNSUPPORTED"
RC_SIGNATURE_INVALID = "DELETE_DENIED_SIGNATURE_INVALID"
RC_KEY_REVOKED = "DELETE_DENIED_KEY_REVOKED"
RC_OPERATION_MISMATCH = "DELETE_DENIED_OPERATION_MISMATCH"
RC_MISSION_MISMATCH = "DELETE_DENIED_MISSION_MISMATCH"
RC_REQUEST_MISMATCH = "DELETE_DENIED_REQUEST_MISMATCH"
RC_OBJECT_MISMATCH = "DELETE_DENIED_OBJECT_MISMATCH"
RC_PATH_MISMATCH = "DELETE_DENIED_PATH_MISMATCH"
RC_COMMIT_MISMATCH = "DELETE_DENIED_COMMIT_MISMATCH"
RC_PROVENANCE_MISMATCH = "DELETE_DENIED_PROVENANCE_MISMATCH"
RC_NONCE_MISMATCH = "DELETE_DENIED_NONCE_MISMATCH"
RC_ISSUED_FUTURE = "DELETE_DENIED_ISSUED_FUTURE"
RC_EXPIRED = "DELETE_DENIED_EXPIRED"
RC_CONSUMED = "DELETE_DENIED_CONSUMED"
RC_STATE_DISALLOWED = "DELETE_DENIED_STATE_DISALLOWED"
RC_REPLAY = "DELETE_DENIED_REPLAY"
RC_CROSS_OBJECT = "DELETE_DENIED_CROSS_OBJECT"
RC_CROSS_MISSION = "DELETE_DENIED_CROSS_MISSION"
RC_KEY_ID_UNKNOWN = "DELETE_DENIED_KEY_ID_UNKNOWN"
class ApprovalVerificationError(Exception):
"""Verification fehlgeschlagen (FAIL CLOSED)."""
def __init__(self, reason_code: str, message: str):
super().__init__(message)
self.reason_code = reason_code
self.message = message
class ApprovalVerifier:
"""
Verifiziert eine Human DELETE Approval gegen den erwarteten Kontext.
Der Verifier besitzt NUR Public Keys (verify-only). Er kann KEINE
Approval erzeugen (kein Private Key).
"""
def __init__(
self,
public_keys: Dict[str, Any],
revoked_key_ids: Optional[set] = None,
now_fn=None,
):
"""
public_keys: {key_id: Ed25519PublicKey} — aktuelle + ggf. transitioning Keys.
revoked_key_ids: set von key_ids, die revoked sind.
now_fn: optionaler Zeitgeber (Tests); Default datetime.datetime.now(timezone.utc).
"""
self.public_keys = dict(public_keys)
self.revoked_key_ids = set(revoked_key_ids or set())
self._now_fn = now_fn or (lambda: datetime.datetime.now(datetime.timezone.utc))
def _now(self) -> datetime.datetime:
return self._now_fn()
def verify_approval(
self,
raw_payload: str,
signature: str,
*,
expected_mission_id: str,
expected_delete_request_id: str,
expected_object_id: str,
expected_vault_path: str,
expected_commit: str,
expected_provenance_hash: str,
expected_nonce: str,
consumed: bool = False,
state_allows_delete: bool = True,
used_nonces: Optional[set] = None,
) -> Dict[str, Any]:
"""
Führt ALLE §8-Checks aus. Wirft ApprovalVerificationError (DELETE_DENIED)
bei jedem Fehler. Gibt bei Erfolg ein dict mit Verifikations-Ergebnis zurück.
"""
used_nonces = used_nonces or set()
# 1. Schema gültig (kanonischer Payload)
try:
payload = parse_payload(raw_payload)
except ApprovalPayloadError as e:
raise ApprovalVerificationError(RC_SCHEMA_INVALID, str(e)) from e
# 2. Unterstützte Approval-Version
if payload.get("approval_version") != APPROVAL_VERSION:
raise ApprovalVerificationError(
RC_VERSION_UNSUPPORTED,
f"Unsupported approval_version: {payload.get('approval_version')}",
)
# 3. Signatur gültig (Ed25519, Public Key)
# Zuerst KEY_ID aus dem Payload ableiten? Nein — KEY_ID wird separat übergeben.
# Wir prüfen gegen ALLE bekannten (nicht-revoked) Public Keys.
sig_valid = False
matched_key_id = None
for kid, pub in self.public_keys.items():
if kid in self.revoked_key_ids:
continue
if verify(pub, payload, signature):
sig_valid = True
matched_key_id = kid
break
if not sig_valid:
raise ApprovalVerificationError(RC_SIGNATURE_INVALID, "Signatur ungültig oder Key unbekannt/revoked")
# 4. Key nicht revoked (bereits in Schleife geprüft)
# 5. Operation == DELETE
if payload.get("operation") != OPERATION_DELETE:
raise ApprovalVerificationError(
RC_OPERATION_MISMATCH,
f"operation muss {OPERATION_DELETE} sein, nicht {payload.get('operation')}",
)
# 6. Mission stimmt
if payload.get("mission_id") != expected_mission_id:
raise ApprovalVerificationError(
RC_MISSION_MISMATCH,
f"mission_id {payload.get('mission_id')} != erwartet {expected_mission_id}",
)
# 7. Delete Request stimmt
if payload.get("delete_request_id") != expected_delete_request_id:
raise ApprovalVerificationError(
RC_REQUEST_MISMATCH,
f"delete_request_id {payload.get('delete_request_id')} != erwartet {expected_delete_request_id}",
)
# 8. Object stimmt
if payload.get("object_id") != expected_object_id:
raise ApprovalVerificationError(
RC_OBJECT_MISMATCH,
f"object_id {payload.get('object_id')} != erwartet {expected_object_id}",
)
# 9. Path stimmt
if payload.get("vault_path") != expected_vault_path:
raise ApprovalVerificationError(
RC_PATH_MISMATCH,
f"vault_path {payload.get('vault_path')} != erwartet {expected_vault_path}",
)
# 10. Commit stimmt
if payload.get("expected_commit") != expected_commit:
raise ApprovalVerificationError(
RC_COMMIT_MISMATCH,
f"expected_commit {payload.get('expected_commit')} != erwartet {expected_commit}",
)
# 11. Provenance stimmt
if payload.get("expected_provenance_hash") != expected_provenance_hash:
raise ApprovalVerificationError(
RC_PROVENANCE_MISMATCH,
f"expected_provenance_hash {payload.get('expected_provenance_hash')} != erwartet {expected_provenance_hash}",
)
# 12. Nonce stimmt
if payload.get("nonce") != expected_nonce:
raise ApprovalVerificationError(
RC_NONCE_MISMATCH,
f"nonce {payload.get('nonce')} != erwartet {expected_nonce}",
)
# 13. issued_at plausibel (nicht in der Zukunft)
issued = _parse_iso(payload["issued_at"])
if issued > self._now() + datetime.timedelta(minutes=5):
raise ApprovalVerificationError(RC_ISSUED_FUTURE, "issued_at liegt in der Zukunft")
# 14. expires_at nicht überschritten
expires = _parse_iso(payload["expires_at"])
if expires < self._now():
raise ApprovalVerificationError(RC_EXPIRED, "Approval ist abgelaufen")
# 15. Approval noch nicht consumed
if consumed:
raise ApprovalVerificationError(RC_CONSUMED, "Approval ist bereits consumed")
# 16. State erlaubt Delete
if not state_allows_delete:
raise ApprovalVerificationError(RC_STATE_DISALLOWED, "State erlaubt kein Delete")
# 17. kein Replay (Nonce bereits verwendet)
if payload["nonce"] in used_nonces:
raise ApprovalVerificationError(RC_REPLAY, "Nonce wurde bereits verwendet (Replay)")
# 18. kein Cross-Object-Reuse (Object-Binding bereits geprüft)
# 19. kein Cross-Mission-Reuse (Mission-Binding bereits geprüft)
return {
"verified": True,
"key_id": matched_key_id,
"approval_id": payload["approval_id"],
"nonce": payload["nonce"],
"payload_hash": _payload_hash(payload),
}
def _parse_iso(value: str) -> datetime.datetime:
"""Parst ISO-8601-UTC (…Z) zu datetime (UTC)."""
# Format: YYYY-MM-DDTHH:MM:SSZ
dt = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
return dt.replace(tzinfo=datetime.timezone.utc)
def _payload_hash(payload: Dict[str, Any]) -> str:
from approval_payload import payload_hash
return payload_hash(payload)