""" 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