Source code for anza.crypto.ed25519

"""Ed25519 signing and verification (available as ``anza.ed25519``).

Backed by the ``cryptography`` package (constant-time, RFC 8032). Keys are
exactly 32 bytes, 64-char lowercase hex, or ``cryptography`` key objects;
anything else is rejected. Signatures are 128 lowercase hex characters.
"""

from typing import Any, Tuple, Union

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat

from ..errors import AnzaError
from .hex import decode_length
from .signer import private_key, public_key, raw_public_key


[docs] def keypair() -> Tuple[bytes, bytes]: """Generate a new Ed25519 key pair. Returns: tuple[bytes, bytes]: ``(private_seed, public_key)``, 32 bytes each. Keep the seed secret; give verifiers the public key. Example: >>> from anza import ed25519 >>> seed, public = ed25519.keypair() >>> len(seed), len(public) (32, 32) """ key = Ed25519PrivateKey.generate() seed = key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()) return seed, raw_public_key(key)
[docs] def sign(key: Any, data: Union[str, bytes]) -> str: """Sign ``data`` with an Ed25519 private key. Args: key: A 32-byte seed, 64-char lowercase hex, or an ``Ed25519PrivateKey``. data: The message; a ``str`` is encoded as UTF-8. Returns: str: The signature, 128 lowercase hex characters. Raises: AnzaError: ``CRYPTO`` for a malformed key. Example: >>> from anza import ed25519 >>> seed, public = ed25519.keypair() >>> sig = ed25519.sign(seed, "hi") >>> len(sig), ed25519.verify(public, "hi", sig), ed25519.verify(public, "ho", sig) (128, True, False) """ data_bytes = data.encode("utf-8") if isinstance(data, str) else bytes(data) try: return private_key(key).sign(data_bytes).hex() except AnzaError: raise except Exception as e: # pragma: no cover - defensive raise AnzaError.crypto(f"Ed25519 signing failed: {type(e).__name__}") from None
[docs] def verify(key: Any, data: Union[str, bytes], signature_hex: str) -> bool: """Verify a lowercase-hex Ed25519 signature. Args: key: The public key: 32 bytes, 64-char lowercase hex, or an ``Ed25519PublicKey``. data: The signed message. signature_hex: The signature to check. Returns: bool: ``True`` when it is valid. Malformed keys or signatures (wrong length, non-hex, uppercase) return ``False`` rather than raising. """ sig = decode_length(signature_hex, 64) if sig is None: return False try: pub = public_key(key) except AnzaError: return False data_bytes = data.encode("utf-8") if isinstance(data, str) else bytes(data) try: pub.verify(sig, data_bytes) return True except InvalidSignature: return False