diff --git a/src/lean_spec/subspecs/xmss/__init__.py b/src/lean_spec/subspecs/xmss/__init__.py new file mode 100644 index 00000000..9e381730 --- /dev/null +++ b/src/lean_spec/subspecs/xmss/__init__.py @@ -0,0 +1,22 @@ +""" +This package provides a Python specification for the Generalized XMSS +hash-based signature scheme. + +It exposes the core data structures and the main interface functions. +""" + +from .constants import LIFETIME, MESSAGE_LENGTH +from .interface import key_gen, sign, verify +from .structures import HashTreeOpening, PublicKey, SecretKey, Signature + +__all__ = [ + "key_gen", + "sign", + "verify", + "PublicKey", + "Signature", + "SecretKey", + "HashTreeOpening", + "LIFETIME", + "MESSAGE_LENGTH", +] diff --git a/src/lean_spec/subspecs/xmss/constants.py b/src/lean_spec/subspecs/xmss/constants.py new file mode 100644 index 00000000..e2f14bb0 --- /dev/null +++ b/src/lean_spec/subspecs/xmss/constants.py @@ -0,0 +1,101 @@ +""" +Defines the cryptographic constants for the XMSS specification. + +This specification corresponds to the "hashing-optimized" Top Level Target Sum +instantiation from the canonical Rust implementation. + +.. note:: + This specification uses the **KoalaBear** prime field, which is consistent + with the formal analysis in the reference papers (e.g., Section 5 of the + "LeanSig" technical note: https://eprint.iacr.org/2025/1332). + + The canonical Rust implementation currently uses the `BabyBear` field for + practical reasons but is expected to align with this + specification in the future. +""" + +from ..koalabear import Fp + +# ================================================================= +# Core Scheme Configuration +# ================================================================= + +MESSAGE_LENGTH: int = 32 +"""The length in bytes for all messages to be signed.""" + +LOG_LIFETIME: int = 32 +"""The base-2 logarithm of the scheme's maximum lifetime.""" + +LIFETIME: int = 1 << LOG_LIFETIME +""" +The maximum number of epochs supported by this configuration. + +An individual key pair can be active for a smaller sub-range. +""" + + +# ================================================================= +# Target Sum WOTS Parameters +# ================================================================= + +DIMENSION: int = 64 +"""The total number of hash chains, `v`.""" + +BASE: int = 8 +"""The alphabet size for the digits of the encoded message.""" + +FINAL_LAYER: int = 77 +"""The number of top layers of the hypercube to map the hash output into.""" + +TARGET_SUM: int = 375 +"""The required sum of all codeword chunks for a signature to be valid.""" + + +# ================================================================= +# Hash and Encoding Length Parameters (in field elements) +# ================================================================= + +PARAMETER_LEN: int = 5 +""" +The length of the public parameter `P`. + +It is used to specialize the hash function. +""" + +TWEAK_LEN_FE: int = 2 +"""The length of a domain-separating tweak.""" + +MSG_LEN_FE: int = 9 +"""The length of a message after being encoded into field elements.""" + +RAND_LEN_FE: int = 7 +"""The length of the randomness `rho` used during message encoding.""" + +HASH_LEN_FE: int = 8 +"""The output length of the main tweakable hash function.""" + +CAPACITY: int = 9 +"""The capacity of the Poseidon2 sponge, defining its security level.""" + +POS_OUTPUT_LEN_PER_INV_FE: int = 15 +"""Output length per invocation for the message hash.""" + +POS_INVOCATIONS: int = 1 +"""Number of invocations for the message hash.""" + +POS_OUTPUT_LEN_FE: int = POS_OUTPUT_LEN_PER_INV_FE * POS_INVOCATIONS +"""Total output length for the message hash.""" + + +# ================================================================= +# Domain Separator Prefixes for Tweaks +# ================================================================= + +TWEAK_PREFIX_CHAIN = Fp(value=0x00) +"""The unique prefix for tweaks used in Winternitz-style hash chains.""" + +TWEAK_PREFIX_TREE = Fp(value=0x01) +"""The unique prefix for tweaks used when hashing Merkle tree nodes.""" + +TWEAK_PREFIX_MESSAGE = Fp(value=0x02) +"""The unique prefix for tweaks used in the initial message hashing step.""" diff --git a/src/lean_spec/subspecs/xmss/interface.py b/src/lean_spec/subspecs/xmss/interface.py new file mode 100644 index 00000000..e8e7e492 --- /dev/null +++ b/src/lean_spec/subspecs/xmss/interface.py @@ -0,0 +1,109 @@ +""" +Defines the core interface for the Generalized XMSS signature scheme. + +Specification for the high-level functions (`key_gen`, `sign`, `verify`) +that constitute the public API of the signature scheme. For the purpose of this +specification, these are defined as placeholders with detailed documentation. +""" + +from __future__ import annotations + +from typing import Tuple + +from .structures import PublicKey, SecretKey, Signature + + +def key_gen( + activation_epoch: int, num_active_epochs: int +) -> Tuple[PublicKey, SecretKey]: + """ + Generates a new cryptographic key pair. This is a **randomized** algorithm. + + This function is a placeholder. In a real implementation, it would involve + generating a master secret, deriving all one-time keys, and constructing + the full Merkle tree. + + Args: + activation_epoch: The starting epoch for which this key is active. + num_active_epochs: The number of consecutive epochs + the key is active for. + + For the formal specification of this process, please refer to: + - "Hash-Based Multi-Signatures for Post-Quantum Ethereum": https://eprint.iacr.org/2025/055 + - "Technical Note: LeanSig for Post-Quantum Ethereum": https://eprint.iacr.org/2025/1332 + - The canonical Rust implementation: https://github.com/b-wagn/hash-sig + """ + raise NotImplementedError( + "key_gen is not part of this specification. " + "See the Rust reference implementation." + ) + + +def sign(sk: SecretKey, epoch: int, message: bytes) -> Signature: + """ + Produces a digital signature for a given message at a specific epoch. This + is a **randomized** algorithm. + + This function is a placeholder. The signing process involves encoding the + message, generating a one-time signature, and providing a Merkle path. + + **CRITICAL**: This function must never be called twice with the same secret + key and epoch for different messages, as this would compromise security. + + For the formal specification of this process, please refer to: + - "Hash-Based Multi-Signatures for Post-Quantum Ethereum": https://eprint.iacr.org/2025/055 + - "Technical Note: LeanSig for Post-Quantum Ethereum": https://eprint.iacr.org/2025/1332 + - The canonical Rust implementation: https://github.com/b-wagn/hash-sig + """ + raise NotImplementedError( + "sign is not part of this specification. " + "See the Rust reference implementation." + ) + + +def verify(pk: PublicKey, epoch: int, message: bytes, sig: Signature) -> bool: + r""" + Verifies a digital signature against a public key, message, and epoch. This + is a **deterministic** algorithm. + + This function is a placeholder. The complete verification logic is detailed + below and will be implemented in a future update. + + ### Verification Algorithm + + 1. **Re-encode Message**: The verifier uses the randomness `rho` from the + signature to re-compute the codeword $x = (x_1, \dots, x_v)$ from the + message `m`. + This includes calculating the checksum or checking the target sum. + + 2. **Reconstruct One-Time Public Key**: For each intermediate hash $y_i$ + in the signature, the verifier completes the corresponding hash chain. + Since $y_i$ was computed with $x_i$ steps, the verifier applies the + hash function an additional $w - 1 - x_i$ times to arrive at the + one-time public key component $pk_{ep,i}$. + + 3. **Compute Merkle Leaf**: The verifier hashes the reconstructed one-time + public key components to compute the expected Merkle leaf for `epoch`. + + 4. **Verify Merkle Path**: The verifier uses the `path` from the signature + to compute a candidate Merkle root starting from the computed leaf. + Verification succeeds if and only if this candidate root matches the + `root` in the `PublicKey`. + + Args: + pk: The public key to verify against. + epoch: The epoch the signature corresponds to. + message: The message that was supposedly signed. + sig: The signature object to be verified. + + Returns: + `True` if the signature is valid, `False` otherwise. + + For the formal specification of this process, please refer to: + - "Hash-Based Multi-Signatures for Post-Quantum Ethereum": https://eprint.iacr.org/2025/055 + - "Technical Note: LeanSig for Post-Quantum Ethereum": https://eprint.iacr.org/2025/1332 + - The canonical Rust implementation: https://github.com/b-wagn/hash-sig + """ + raise NotImplementedError( + "verify will be implemented in a future update to the specification." + ) diff --git a/src/lean_spec/subspecs/xmss/structures.py b/src/lean_spec/subspecs/xmss/structures.py new file mode 100644 index 00000000..306e9948 --- /dev/null +++ b/src/lean_spec/subspecs/xmss/structures.py @@ -0,0 +1,75 @@ +"""Defines the data structures for the Generalized XMSS signature scheme.""" + +from typing import Annotated, List + +from pydantic import BaseModel, ConfigDict, Field + +from ..koalabear import Fp +from .constants import HASH_LEN_FE, PARAMETER_LEN, RAND_LEN_FE + +HashDigest = Annotated[ + List[Fp], Field(min_length=HASH_LEN_FE, max_length=HASH_LEN_FE) +] +""" +A type alias representing a hash digest. +""" + +Parameter = Annotated[ + List[Fp], Field(min_length=PARAMETER_LEN, max_length=PARAMETER_LEN) +] +""" +A type alias representing the public parameter `P`. +""" + +Randomness = Annotated[ + List[Fp], Field(min_length=RAND_LEN_FE, max_length=RAND_LEN_FE) +] +""" +A type alias representing the randomness `rho`. +""" + + +class HashTreeOpening(BaseModel): + """ + A Merkle authentication path. + + It contains a list of sibling nodes required to reconstruct the path + from a leaf node up to the Merkle root. + """ + + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + siblings: List[HashDigest] = Field( + ..., description="List of sibling hashes, from bottom to top." + ) + + +class PublicKey(BaseModel): + """The public key for the Generalized XMSS scheme.""" + + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + root: List[Fp] = Field(..., max_length=HASH_LEN_FE, min_length=HASH_LEN_FE) + parameter: Parameter = Field( + ..., max_length=PARAMETER_LEN, min_length=PARAMETER_LEN + ) + + +class Signature(BaseModel): + """A signature in the Generalized XMSS scheme.""" + + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + path: HashTreeOpening + rho: Randomness = Field( + ..., max_length=RAND_LEN_FE, min_length=RAND_LEN_FE + ) + hashes: List[HashDigest] + + +class SecretKey(BaseModel): + """ + Placeholder for the secret key. + + Note: The full secret key structure is not specified here as it is not + needed for verification. + """ + + pass