Source code for httk.core.crypto

"""Ed25519 signing with an optional accelerated backend.

These signing primitives are used by project identity and trust.

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. :param backend: Backend name: ``"cryptography"``, ``"pure"``, or ``"stdlib"``. :return: Whether the selected backend is available. :raises ValueError: If ``backend`` is not a supported backend name. """ 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: A newly generated private seed. """ return secrets.token_bytes(32)
[docs] def ed25519_public_key(seed: bytes, *, backend: str | None = None) -> bytes: """Derive the public key for a private seed. :param seed: 32-byte Ed25519 private seed. :param backend: Backend name, ``"auto"``, or ``None`` to select the accelerated backend when available. :return: The 32-byte Ed25519 public key. :raises ValueError: If ``seed`` is not exactly 32 bytes or ``backend`` is unsupported. :raises ImportError: If the requested ``cryptography`` backend is unavailable. """ 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``. :param seed: 32-byte Ed25519 private seed. :param message: Bytes to sign. :param backend: Backend name, ``"auto"``, or ``None`` to select the accelerated backend when available. :return: The 64-byte Ed25519 signature. :raises ValueError: If ``seed`` is not exactly 32 bytes or ``backend`` is unsupported. :raises ImportError: If the requested ``cryptography`` backend is unavailable. """ 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. :param public_key: 32-byte Ed25519 public key. :param message: Bytes whose signature is being checked. :param signature: 64-byte Ed25519 signature. :param backend: Backend name, ``"auto"``, or ``None`` to select the accelerated backend when available. :return: Whether the signature verifies for the public key and message. :raises ValueError: If ``backend`` is unsupported. :raises ImportError: If the requested ``cryptography`` backend is unavailable. """ 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.