"""Ed25519 identities and signature verification. A node identity is an ed25519 keypair. The public author id is ``mnk_``. Signatures are detached ed25519 signatures over MNC-1 canonical bytes, hex-encoded on the wire. Targets PyNaCl >= 1.5 (``nacl.signing.SigningKey`` / ``VerifyKey``), a stable API since PyNaCl 1.0. """ from __future__ import annotations from dataclasses import dataclass, field from nacl.exceptions import BadSignatureError from nacl.signing import SigningKey, VerifyKey from mnema.core.ids import AUTHOR_ID_PREFIX, is_author_id __all__ = ["Identity", "verify_signature", "verify_signature_strict", "KeyError_"] class KeyError_(ValueError): """Raised for malformed keys, author ids, or signatures. (Named with a trailing underscore to avoid shadowing the builtin.) """ @dataclass(frozen=True) class Identity: """A signing identity owned by a user node or device.""" signing_key: SigningKey = field(repr=False) @classmethod def generate(cls) -> "Identity": return cls(signing_key=SigningKey.generate()) @classmethod def from_seed_hex(cls, seed_hex: str) -> "Identity": """Deterministic identity from a 32-byte hex seed (tests/fixtures).""" seed = bytes.fromhex(seed_hex) if len(seed) != 32: raise KeyError_("seed must be exactly 32 bytes of hex") return cls(signing_key=SigningKey(seed)) def to_seed_hex(self) -> str: return bytes(self.signing_key).hex() @property def verify_key(self) -> VerifyKey: return self.signing_key.verify_key @property def author_id(self) -> str: return AUTHOR_ID_PREFIX + bytes(self.verify_key).hex() def sign(self, data: bytes) -> str: """Detached signature over *data*, hex-encoded.""" return self.signing_key.sign(data).signature.hex() def _verify_key_for(author_id: str) -> VerifyKey: if not is_author_id(author_id): raise KeyError_(f"malformed author id: {author_id!r}") return VerifyKey(bytes.fromhex(author_id[len(AUTHOR_ID_PREFIX):])) def verify_signature(author_id: str, data: bytes, sig_hex: str) -> bool: """True iff *sig_hex* is a valid signature by *author_id* over *data*.""" try: verify_signature_strict(author_id, data, sig_hex) return True except (KeyError_, BadSignatureError, ValueError): return False def verify_signature_strict(author_id: str, data: bytes, sig_hex: str) -> None: """Verify a signature; raises on failure.""" key = _verify_key_for(author_id) try: signature = bytes.fromhex(sig_hex) except ValueError as exc: raise KeyError_(f"signature is not valid hex: {exc}") from exc if len(signature) != 64: raise KeyError_("ed25519 signature must be 64 bytes") key.verify(data, signature) # raises BadSignatureError on mismatch