"""Cryptographic primitives for the mesh layer. Ed25519 for signing (membership events, gossip envelopes). X25519 for key agreement (wrap epoch secrets per-member). ChaCha20-Poly1305 for AEAD (optional payload encryption). All key material is bytes (raw 32-byte forms) so the storage layer can keep keys in BLOB columns without serialization. The `cryptography` library is the audited backend; this module is a thin wrapper that hides the import surface and enforces consistent error handling. """ from __future__ import annotations from cryptography.exceptions import InvalidSignature, InvalidTag from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF # ----------------------------------------------------------------------------- # Ed25519 — signing # ----------------------------------------------------------------------------- def generate_signing_keypair() -> tuple[bytes, bytes]: """Return (priv_bytes, pub_bytes) for a fresh Ed25519 keypair.""" priv = ed25519.Ed25519PrivateKey.generate() priv_bytes = priv.private_bytes_raw() pub_bytes = priv.public_key().public_bytes_raw() return priv_bytes, pub_bytes def sign(priv_bytes: bytes, message: bytes) -> bytes: """Ed25519 sign. 64-byte signature.""" priv = ed25519.Ed25519PrivateKey.from_private_bytes(priv_bytes) return priv.sign(message) def verify(pub_bytes: bytes, signature: bytes, message: bytes) -> bool: """Ed25519 verify. Returns True/False — never raises.""" try: pub = ed25519.Ed25519PublicKey.from_public_bytes(pub_bytes) pub.verify(signature, message) return True except (InvalidSignature, ValueError): return False # ----------------------------------------------------------------------------- # X25519 — key agreement for wrapping epoch secrets per-member # ----------------------------------------------------------------------------- def generate_dh_keypair() -> tuple[bytes, bytes]: """Return (priv_bytes, pub_bytes) for a fresh X25519 keypair.""" priv = x25519.X25519PrivateKey.generate() priv_bytes = priv.private_bytes_raw() pub_bytes = priv.public_key().public_bytes_raw() return priv_bytes, pub_bytes def ecdh_shared_secret(priv_bytes: bytes, peer_pub_bytes: bytes) -> bytes: """X25519 ECDH -> 32-byte shared secret (HKDF-extracted).""" priv = x25519.X25519PrivateKey.from_private_bytes(priv_bytes) peer = x25519.X25519PublicKey.from_public_bytes(peer_pub_bytes) raw = priv.exchange(peer) # HKDF-Extract+Expand to a 32-byte AEAD key. The salt is fixed; the # info string distinguishes this key from any other use of ECDH on # the same peer-pair (e.g. if we ever add another protocol layer). return HKDF( algorithm=hashes.SHA256(), length=32, salt=b"aborist.mesh.epoch.v1", info=b"epoch-secret-wrap", ).derive(raw) # ----------------------------------------------------------------------------- # AEAD — ChaCha20-Poly1305 for envelope payloads + secret wrapping # ----------------------------------------------------------------------------- def aead_encrypt(key: bytes, nonce: bytes, plaintext: bytes, aad: bytes = b"") -> bytes: """Encrypt + authenticate. Returns ciphertext||tag.""" if len(key) != 32: raise ValueError("AEAD key must be 32 bytes") if len(nonce) != 12: raise ValueError("ChaCha20-Poly1305 nonce must be 12 bytes") cipher = ChaCha20Poly1305(key) return cipher.encrypt(nonce, plaintext, aad) def aead_decrypt(key: bytes, nonce: bytes, ciphertext: bytes, aad: bytes = b"") -> bytes: """Decrypt + verify. Raises ValueError on tag mismatch (no plaintext leak).""" if len(key) != 32: raise ValueError("AEAD key must be 32 bytes") if len(nonce) != 12: raise ValueError("ChaCha20-Poly1305 nonce must be 12 bytes") cipher = ChaCha20Poly1305(key) try: return cipher.decrypt(nonce, ciphertext, aad) except InvalidTag as e: raise ValueError("AEAD authentication failed") from e