"""Ed25519 keys and envelope signatures. The signature payload is the canonical serialization of the envelope *without* its ``sig`` member. The operation id is the SHA-256 of the canonical serialization of the envelope *with* its ``sig`` member. See spec/02-wire-format/04-keys-and-capabilities.md. """ from nacl.exceptions import BadSignatureError from nacl.signing import SigningKey, VerifyKey from .canonical import canonicalize from .errors import FpcfError, E_SIG from .ids import PUBKEY_PREFIX SIG_PREFIX = "ed25519:" SEED_BYTES = 32 def generate_keypair() -> SigningKey: """Generate a fresh random Ed25519 signing key.""" return SigningKey.generate() def keypair_from_seed(seed: bytes) -> SigningKey: """Deterministic Ed25519 signing key from a 32-byte seed.""" if len(seed) != SEED_BYTES: raise ValueError("seed must be exactly %d bytes" % SEED_BYTES) return SigningKey(seed) def public_str(key) -> str: """``ed25519:`` public-key string for a SigningKey or VerifyKey.""" if isinstance(key, SigningKey): key = key.verify_key if not isinstance(key, VerifyKey): raise TypeError("expected SigningKey or VerifyKey") return PUBKEY_PREFIX + bytes(key).hex() def signature_payload(envelope: dict) -> bytes: """Canonical bytes of the envelope with ``sig`` removed.""" unsigned = {k: v for k, v in envelope.items() if k != "sig"} return canonicalize(unsigned) def sign_envelope(envelope: dict, signing_key: SigningKey) -> dict: """Return a copy of ``envelope`` with a fresh ``sig`` member. Any existing ``sig`` member is discarded. The caller is responsible for ``author`` matching ``signing_key`` (``verify_signature`` will fail otherwise). """ unsigned = {k: v for k, v in envelope.items() if k != "sig"} sig = signing_key.sign(canonicalize(unsigned)).signature out = dict(unsigned) out["sig"] = SIG_PREFIX + sig.hex() return out def make_envelope(op_type: str, body: dict, signing_key: SigningKey, *, ts: str, prev=()) -> dict: """Build and sign a complete operation envelope.""" env = { "v": 1, "type": op_type, "author": public_str(signing_key), "ts": ts, "prev": list(prev), "body": body, } return sign_envelope(env, signing_key) def verify_signature(envelope: dict) -> None: """Verify ``envelope["sig"]`` against ``envelope["author"]``. Raises ``FP-E-SIG`` on any failure. Format checks here are defensive; the envelope schema is the authoritative format check and runs earlier in the pipeline. """ author = envelope.get("author") sig = envelope.get("sig") if not isinstance(author, str) or not author.startswith(PUBKEY_PREFIX): raise FpcfError(E_SIG, "author is not an ed25519 public-key string") if not isinstance(sig, str) or not sig.startswith(SIG_PREFIX): raise FpcfError(E_SIG, "sig is not an ed25519 signature string") try: key_bytes = bytes.fromhex(author[len(PUBKEY_PREFIX):]) sig_bytes = bytes.fromhex(sig[len(SIG_PREFIX):]) if len(key_bytes) != 32 or len(sig_bytes) != 64: raise ValueError("bad length") VerifyKey(key_bytes).verify(signature_payload(envelope), sig_bytes) except (ValueError, BadSignatureError) as exc: raise FpcfError(E_SIG, "signature verification failed: %s" % exc)