Source code for httk.core.ed25519

"""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