Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/lean_spec/subspecs/xmss/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
101 changes: 101 additions & 0 deletions src/lean_spec/subspecs/xmss/constants.py
Original file line number Diff line number Diff line change
@@ -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."""
109 changes: 109 additions & 0 deletions src/lean_spec/subspecs/xmss/interface.py
Original file line number Diff line number Diff line change
@@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: in a future impl I would maybe use the same sub-algorithms as in either the rust code or in the paper, and then maybe even point to the pages of the paper.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that's fine, maybe once done, we can just modify the doc here to point to this.

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."
)
75 changes: 75 additions & 0 deletions src/lean_spec/subspecs/xmss/structures.py
Original file line number Diff line number Diff line change
@@ -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
Loading