- 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.
124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
"""
|
|
AUTH.3D — approval_signature.py
|
|
Ed25519 Signatur (RFC 8032) für Human DELETE Approvals.
|
|
|
|
- Christian besitzt den PRIVATE KEY (NIE im Stack).
|
|
- DELETE Executor kennt nur den PUBLIC KEY (verify-only).
|
|
- RQ/Hermes/SAVE/DELETE-Executor besitzen KEINEN Private Key.
|
|
|
|
NUR isolierter Code + Tests mit synthetischen Test-Keypairs.
|
|
KEIN echter Key wird erzeugt. KEIN produktives Deployment.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
from typing import Optional, Tuple
|
|
|
|
from cryptography.exceptions import InvalidSignature
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
Ed25519PrivateKey,
|
|
Ed25519PublicKey,
|
|
)
|
|
|
|
from approval_payload import canonicalize
|
|
|
|
# Signaturformat: Ed25519 (64 Bytes), Base64-URL-encoded.
|
|
SIGNATURE_ALGORITHM = "Ed25519"
|
|
|
|
|
|
class ApprovalSignatureError(ValueError):
|
|
"""Fehler bei Signatur-Erzeugung/Verifikation."""
|
|
|
|
|
|
def generate_test_keypair() -> Tuple[Ed25519PrivateKey, Ed25519PublicKey]:
|
|
"""
|
|
Erzeugt ein SYNTHETISCHES Test-Keypair (NUR für Tests).
|
|
|
|
NIEMALS für produktive Approvals verwenden. Kein echter Key.
|
|
"""
|
|
private = Ed25519PrivateKey.generate()
|
|
return private, private.public_key()
|
|
|
|
|
|
def key_id(public_key: Ed25519PublicKey) -> str:
|
|
"""KEY_ID = SHA-256 über die Public-Key-Bytes (hex, kurz)."""
|
|
raw = public_key.public_bytes(
|
|
encoding=serialization.Encoding.Raw,
|
|
format=serialization.PublicFormat.Raw,
|
|
)
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
|
|
def sign(private_key: Ed25519PrivateKey, payload: dict) -> str:
|
|
"""
|
|
Signiert den kanonisierten Payload mit dem Private Key.
|
|
|
|
Gibt die Signatur als Base64-URL-String zurück.
|
|
"""
|
|
canonical = canonicalize(payload)
|
|
sig = private_key.sign(canonical)
|
|
return base64.urlsafe_b64encode(sig).decode("ascii")
|
|
|
|
|
|
def verify(public_key: Ed25519PublicKey, payload: dict, signature: str) -> bool:
|
|
"""
|
|
Verifiziert die Signatur über den kanonisierten Payload.
|
|
|
|
Gibt True bei gültiger Signatur, False bei ungültiger/malformed.
|
|
FAIL CLOSED: jede Abweichung -> False.
|
|
"""
|
|
if not isinstance(signature, str) or not signature:
|
|
return False
|
|
try:
|
|
sig_bytes = base64.urlsafe_b64decode(signature.encode("ascii"))
|
|
except Exception:
|
|
return False
|
|
if len(sig_bytes) != 64:
|
|
return False
|
|
try:
|
|
canonical = canonicalize(payload)
|
|
except Exception:
|
|
return False
|
|
try:
|
|
public_key.verify(sig_bytes, canonical)
|
|
return True
|
|
except InvalidSignature:
|
|
return False
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def public_key_to_pem(public_key: Ed25519PublicKey) -> bytes:
|
|
"""Serialisiert den Public Key zu PEM (für Verifier-Konfiguration)."""
|
|
return public_key.public_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
)
|
|
|
|
|
|
def public_key_from_pem(pem: bytes) -> Ed25519PublicKey:
|
|
"""Lädt einen Public Key aus PEM."""
|
|
try:
|
|
return serialization.load_pem_public_key(pem) # type: ignore[return-value]
|
|
except Exception as e:
|
|
raise ApprovalSignatureError(f"Ungültiger Public Key PEM: {e}") from e
|
|
|
|
|
|
def private_key_to_pem(private_key: Ed25519PrivateKey) -> bytes:
|
|
"""Serialisiert den Private Key zu PEM (NUR für Christians Gerät / Tests)."""
|
|
return private_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.PKCS8,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
|
|
|
|
def private_key_from_pem(pem: bytes) -> Ed25519PrivateKey:
|
|
"""Lädt einen Private Key aus PEM (NUR Christians Gerät / Tests)."""
|
|
try:
|
|
return serialization.load_pem_private_key(pem, password=None) # type: ignore[return-value]
|
|
except Exception as e:
|
|
raise ApprovalSignatureError(f"Ungültiger Private Key PEM: {e}") from e
|