From 58d6ecacbbbe400fb6e1ea0200d5f0f37acfc5a6 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:03:26 +0200 Subject: [PATCH] feat(xmss): validate XmssConfig fields at construction Add Field(gt=0) positivity constraints to every numeric XmssConfig field, matching the style used by PoseidonParams. Extend the existing model validator to reject an odd LOG_LIFETIME, since the key splits into a top tree and bottom trees that each cover LOG_LIFETIME / 2 levels. Previously a non-positive field or an odd lifetime exponent only failed deep in tree-building with a cryptic error. Now misconfiguration is caught at construction with a clear message. Defaults are unchanged and all three shipped configs still construct. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lean_spec/spec/crypto/xmss/constants.py | 34 ++++++++++++--------- tests/spec/crypto/xmss/test_constants.py | 32 +++++++++++++++++++ 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/lean_spec/spec/crypto/xmss/constants.py b/src/lean_spec/spec/crypto/xmss/constants.py index 4fb263a45..a991dd901 100644 --- a/src/lean_spec/spec/crypto/xmss/constants.py +++ b/src/lean_spec/spec/crypto/xmss/constants.py @@ -3,7 +3,7 @@ import math from typing import Final, Self -from pydantic import model_validator +from pydantic import Field, model_validator from lean_spec.base import StrictBaseModel from lean_spec.config import LEAN_ENV @@ -15,52 +15,56 @@ class XmssConfig(StrictBaseModel): """A model holding the configuration constants for an XMSS preset.""" - LOG_LIFETIME: int + LOG_LIFETIME: int = Field(gt=0) """The base-2 logarithm of the scheme's maximum lifetime.""" - DIMENSION: int + DIMENSION: int = Field(gt=0) """The total number of hash chains, v.""" - BASE: int + BASE: int = Field(gt=0) """The alphabet size for the digits of the encoded message.""" - Z: int + Z: int = Field(gt=0) """Number of base-BASE digits extracted from each field element.""" - Q: int + Q: int = Field(gt=0) """Quotient such that Q * BASE^Z == P - 1.""" - TARGET_SUM: int + TARGET_SUM: int = Field(gt=0) """The required sum of all codeword chunks for a signature to be valid.""" - MAX_TRIES: int + MAX_TRIES: int = Field(gt=0) """How often one should try at most to resample a random value.""" - PARAMETER_LENGTH: int + PARAMETER_LENGTH: int = Field(gt=0) """The length of the public parameter P. It is used to specialize the hash function.""" - TWEAK_LENGTH_FIELD_ELEMENTS: int + TWEAK_LENGTH_FIELD_ELEMENTS: int = Field(gt=0) """The length of a domain-separating tweak.""" - MESSAGE_LENGTH_FIELD_ELEMENTS: int + MESSAGE_LENGTH_FIELD_ELEMENTS: int = Field(gt=0) """The length of a message after being encoded into field elements.""" - RAND_LENGTH_FIELD_ELEMENTS: int + RAND_LENGTH_FIELD_ELEMENTS: int = Field(gt=0) """The length of the randomness rho used during message encoding.""" - HASH_LENGTH_FIELD_ELEMENTS: int + HASH_LENGTH_FIELD_ELEMENTS: int = Field(gt=0) """The output length of the main tweakable hash function.""" - CAPACITY: int + CAPACITY: int = Field(gt=0) """The capacity of the Poseidon sponge, defining its security level.""" @model_validator(mode="after") def _validate_decomposition(self) -> Self: - """Verify that Q * BASE^Z == P - 1.""" + """Verify that Q * BASE^Z == P - 1 and that LOG_LIFETIME is even.""" if self.Q * self.BASE**self.Z != P - 1: raise ValueError(f"Q * BASE^Z must equal P-1={P - 1}") + # The key splits into a top tree and bottom trees. + # Each covers LOG_LIFETIME / 2 levels, so the lifetime exponent must be even. + if self.LOG_LIFETIME % 2 != 0: + raise ValueError(f"LOG_LIFETIME must be even, got {self.LOG_LIFETIME}") return self @property diff --git a/tests/spec/crypto/xmss/test_constants.py b/tests/spec/crypto/xmss/test_constants.py index f87c6018f..202a8d440 100644 --- a/tests/spec/crypto/xmss/test_constants.py +++ b/tests/spec/crypto/xmss/test_constants.py @@ -52,6 +52,38 @@ def test_decomposition_validator_accepts_valid_product() -> None: assert config.Q * config.BASE**config.Z == P - 1 +def test_non_positive_field_is_rejected() -> None: + """A field constrained to be positive rejects a zero value at construction.""" + kwargs = _valid_config_kwargs() + kwargs["DIMENSION"] = 0 + with pytest.raises(ValueError) as exception_info: + XmssConfig(**kwargs) + assert str(exception_info.value) == ( + "1 validation error for XmssConfig\n" + "DIMENSION\n" + " Input should be greater than 0 " + "[type=greater_than, input_value=0, input_type=int]\n" + " For further information visit " + "https://errors.pydantic.dev/2.12/v/greater_than" + ) + + +def test_odd_log_lifetime_is_rejected() -> None: + """An odd lifetime exponent cannot split into equal top and bottom trees.""" + kwargs = _valid_config_kwargs() + kwargs["LOG_LIFETIME"] = 31 + with pytest.raises(ValueError) as exception_info: + XmssConfig(**kwargs) + assert str(exception_info.value) == ( + "1 validation error for XmssConfig\n" + " Value error, LOG_LIFETIME must be even, got 31 " + "[type=value_error, input_value={'LOG_LIFETIME': 31, 'DIM...ENTS': 8, 'CAPACITY': 9}, " + "input_type=dict]\n" + " For further information visit " + "https://errors.pydantic.dev/2.12/v/value_error" + ) + + def test_target_config_is_test_config_under_test_env() -> None: """The active configuration under the test environment is the test preset.""" assert TARGET_CONFIG is TEST_CONFIG