"""Ed25519 signing with an optional accelerated backend.
All private keys accepted and returned by this module are standard 32-byte
Ed25519 seeds. The stdlib implementation follows RFC 8032 and is kept as a
portable fallback; ``cryptography`` is used automatically when installed.
"""
import hashlib
import importlib
import importlib.util
import secrets
from typing import Any
_P = 2**255 - 19
_L = 2**252 + 27742317777372353535851937790883648493
_D = (-121665 * pow(121666, _P - 2, _P)) % _P
_I = pow(2, (_P - 1) // 4, _P)
_IDENTITY = (0, 1, 1, 0)
def _xrecover(y: int) -> int:
xx = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P)
x = pow(xx, (_P + 3) // 8, _P)
if (x * x - xx) % _P:
x = (x * _I) % _P
if x & 1:
x = _P - x
return x
_BY = (4 * pow(5, _P - 2, _P)) % _P
_BX = _xrecover(_BY)
_BASE = (_BX, _BY, 1, (_BX * _BY) % _P)
def _point_add(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> tuple[int, int, int, int]:
x1, y1, z1, t1 = left
x2, y2, z2, t2 = right
a = ((y1 - x1) * (y2 - x2)) % _P
b = ((y1 + x1) * (y2 + x2)) % _P
c = (2 * _D * t1 * t2) % _P
d = (2 * z1 * z2) % _P
e = b - a
f = d - c
g = d + c
h = b + a
return (e * f % _P, g * h % _P, f * g % _P, e * h % _P)
def _scalar_multiply(point: tuple[int, int, int, int], scalar: int) -> tuple[int, int, int, int]:
result = _IDENTITY
addend = point
while scalar:
if scalar & 1:
result = _point_add(result, addend)
addend = _point_add(addend, addend)
scalar >>= 1
return result
def _encode_point(point: tuple[int, int, int, int]) -> bytes:
x, y, z, _ = point
inverse = pow(z, _P - 2, _P)
affine_x = x * inverse % _P
affine_y = y * inverse % _P
return int.to_bytes(affine_y | ((affine_x & 1) << 255), 32, "little")
def _decode_point(encoded: bytes) -> tuple[int, int, int, int]:
if len(encoded) != 32:
raise ValueError("Ed25519 encoded point must be 32 bytes")
integer = int.from_bytes(encoded, "little")
y = integer & ((1 << 255) - 1)
sign = integer >> 255
if y >= _P:
raise ValueError("non-canonical Ed25519 point")
x = _xrecover(y)
if (x & 1) != sign:
x = _P - x
if x == 0 and sign:
raise ValueError("non-canonical Ed25519 point")
if (-x * x + y * y - 1 - _D * x * x * y * y) % _P:
raise ValueError("point is not on the Ed25519 curve")
point = (x, y, 1, x * y % _P)
if _encode_point(point) != encoded:
raise ValueError("non-canonical Ed25519 point")
return point
def _secret_scalar(seed: bytes) -> tuple[int, bytes]:
digest = hashlib.sha512(seed).digest()
scalar_bytes = bytearray(digest[:32])
scalar_bytes[0] &= 248
scalar_bytes[31] &= 63
scalar_bytes[31] |= 64
return int.from_bytes(scalar_bytes, "little"), digest[32:]
def _hint(value: bytes) -> int:
return int.from_bytes(hashlib.sha512(value).digest(), "little")
def _pure_public_key(seed: bytes) -> bytes:
scalar, _ = _secret_scalar(seed)
return _encode_point(_scalar_multiply(_BASE, scalar))
def _pure_sign(seed: bytes, message: bytes) -> bytes:
scalar, prefix = _secret_scalar(seed)
public_key = _encode_point(_scalar_multiply(_BASE, scalar))
nonce = _hint(prefix + message) % _L
encoded_r = _encode_point(_scalar_multiply(_BASE, nonce))
challenge = _hint(encoded_r + public_key + message) % _L
return encoded_r + int.to_bytes((nonce + challenge * scalar) % _L, 32, "little")
def _pure_verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
if len(public_key) != 32 or len(signature) != 64:
return False
scalar = int.from_bytes(signature[32:], "little")
if scalar >= _L:
return False
try:
public_point = _decode_point(public_key)
r_point = _decode_point(signature[:32])
except ValueError:
return False
# Reject small-order points. This closes the common cofactor-validation
# ambiguity while retaining every RFC 8032 test vector.
if _encode_point(_scalar_multiply(public_point, 8)) == _encode_point(_IDENTITY):
return False
if _encode_point(_scalar_multiply(r_point, 8)) == _encode_point(_IDENTITY):
return False
challenge = _hint(signature[:32] + public_key + message) % _L
left = _scalar_multiply(_BASE, scalar)
right = _point_add(r_point, _scalar_multiply(public_point, challenge))
return _encode_point(left) == _encode_point(right)
def _cryptography_available() -> bool:
return importlib.util.find_spec("cryptography") is not None
def _cryptography_modules() -> tuple[Any, Any, Any]:
serialization = importlib.import_module("cryptography.hazmat.primitives.serialization")
ed25519 = importlib.import_module("cryptography.hazmat.primitives.asymmetric.ed25519")
exceptions = importlib.import_module("cryptography.exceptions")
return serialization, ed25519, exceptions
[docs]
def ed25519_backend_available(backend: str = "cryptography") -> bool:
"""Return whether *backend* can perform Ed25519 operations."""
if backend in {"pure", "stdlib"}:
return True
if backend == "cryptography":
return _cryptography_available()
raise ValueError(f"unknown Ed25519 backend: {backend!r}")
def _backend(backend: str | None) -> str:
if backend is None or backend == "auto":
return "cryptography" if _cryptography_available() else "pure"
if backend not in {"cryptography", "pure", "stdlib"}:
raise ValueError(f"unknown Ed25519 backend: {backend!r}")
if backend == "cryptography" and not _cryptography_available():
raise ImportError("the cryptography Ed25519 backend is not installed")
return "pure" if backend == "stdlib" else backend
def _seed(value: bytes) -> bytes:
result = bytes(value)
if len(result) != 32:
raise ValueError("Ed25519 private seed must be exactly 32 bytes")
return result
[docs]
def ed25519_generate_seed() -> bytes:
"""Generate a standard 32-byte Ed25519 private seed."""
return secrets.token_bytes(32)
[docs]
def ed25519_public_key(seed: bytes, *, backend: str | None = None) -> bytes:
"""Derive the 32-byte public key for a 32-byte private *seed*."""
private_seed = _seed(seed)
if _backend(backend) == "cryptography":
serialization, ed25519, _ = _cryptography_modules()
return (
ed25519.Ed25519PrivateKey.from_private_bytes(private_seed)
.public_key()
.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
)
return _pure_public_key(private_seed)
[docs]
def ed25519_sign(seed: bytes, message: bytes, *, backend: str | None = None) -> bytes:
"""Return an RFC 8032 Ed25519 signature for *message*."""
private_seed = _seed(seed)
payload = bytes(message)
if _backend(backend) == "cryptography":
_, ed25519, _ = _cryptography_modules()
return ed25519.Ed25519PrivateKey.from_private_bytes(private_seed).sign(payload)
return _pure_sign(private_seed, payload)
[docs]
def ed25519_verify(
public_key: bytes,
message: bytes,
signature: bytes,
*,
backend: str | None = None,
) -> bool:
"""Return whether *signature* is valid, treating malformed input as false."""
key = bytes(public_key)
payload = bytes(message)
signed = bytes(signature)
if len(key) != 32 or len(signed) != 64:
return False
if _backend(backend) == "cryptography":
_, ed25519, exceptions = _cryptography_modules()
try:
ed25519.Ed25519PublicKey.from_public_bytes(key).verify(signed, payload)
except (exceptions.InvalidSignature, ValueError):
return False
return True
return _pure_verify(key, payload, signed)
# Verb-first aliases make the operations easy to discover without creating a
# second key representation.
[docs]
generate_ed25519_seed = ed25519_generate_seed
[docs]
derive_ed25519_public_key = ed25519_public_key
[docs]
sign_ed25519 = ed25519_sign
[docs]
verify_ed25519 = ed25519_verify