From e10d0ffc81b7b3da1604657dc2830991e469fd10 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Wed, 25 Mar 2026 17:30:38 +0100 Subject: [PATCH 01/13] Add passthrough for pure ML-DSA to the test driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don't support ML-DSA in libtestdriver1 yet, because it's a copy of the `builtin` driver but ML-DSA is provided by the `pqcp` driver. This means that we can't test “driver-only” ML-DSA builds, but it should be possible to enable ML-DSA in a build that dispatches through the test driver. This is currently impossible because pure ML-DSA is not a sign-the-hash algorithm, but the code in the test driver for signatures assumes that all signature algorithms are sign-the-hash. Fix this in a minimal way by making the test driver activate the fallback mechanism of driver dispatch when the algorithm is pure ML-DSA. (Don't do this for all algorithms that are not sign-the-hash, because in general, we do want the test driver to fail if it's given an algorithm that it doesn't support.) Signed-off-by: Gilles Peskine --- tests/src/drivers/test_driver_signature.c | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/src/drivers/test_driver_signature.c b/tests/src/drivers/test_driver_signature.c index e8dc07655f..010cdfcf46 100644 --- a/tests/src/drivers/test_driver_signature.c +++ b/tests/src/drivers/test_driver_signature.c @@ -39,6 +39,15 @@ #include LIBTESTDRIVER1_PSA_DRIVER_INTERNAL_HEADER(psa_crypto_rsa.h) #endif +#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) +/* For PSA_ALG_IS_ML_DSA. Including this internal header will no longer + * be needed once we add the ML-DSA macro definitions to the public + * headers. + * https://github.com/Mbed-TLS/TF-PSA-Crypto/issues/726 + */ +#include "psa_crypto_mldsa.h" +#endif + #include mbedtls_test_driver_signature_hooks_t @@ -213,6 +222,20 @@ psa_status_t mbedtls_test_transparent_signature_sign_message( return PSA_SUCCESS; } +#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) + /* Pure ML-DSA is not a sign-the-hash algorithm. At the moment, this + * function only knows how to deal with sign-the-hash algorithms. + * So give up and let the next driver in the chain handle the algorithm. + * For pure ML-DSA, this will be the pqcp driver, which does not have + * a libtestdriver1 variant, meaning that we can't test "driver-only" + * builds for pure ML-DSA, but we can have ML-DSA enabled in builds that + * dispatch through the test driver. + */ + if (PSA_ALG_IS_ML_DSA(alg)) { + return PSA_ERROR_NOT_SUPPORTED; + } +#endif + #if defined(MBEDTLS_TEST_LIBTESTDRIVER1) && \ defined(LIBTESTDRIVER1_MBEDTLS_PSA_BUILTIN_HASH) status = libtestdriver1_mbedtls_psa_hash_compute( @@ -280,6 +303,20 @@ psa_status_t mbedtls_test_transparent_signature_verify_message( return mbedtls_test_driver_signature_verify_hooks.forced_status; } +#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) + /* Pure ML-DSA is not a sign-the-hash algorithm. At the moment, this + * function only knows how to deal with sign-the-hash algorithms. + * So give up and let the next driver in the chain handle the algorithm. + * For pure ML-DSA, this will be the pqcp driver, which does not have + * a libtestdriver1 variant, meaning that we can't test "driver-only" + * builds for pure ML-DSA, but we can have ML-DSA enabled in builds that + * dispatch through the test driver. + */ + if (PSA_ALG_IS_ML_DSA(alg)) { + return PSA_ERROR_NOT_SUPPORTED; + } +#endif + #if defined(MBEDTLS_TEST_LIBTESTDRIVER1) && \ defined(LIBTESTDRIVER1_MBEDTLS_PSA_BUILTIN_HASH) status = libtestdriver1_mbedtls_psa_hash_compute( From c8672388d822df1c96dd9da70775b7698704649e Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Thu, 26 Mar 2026 11:22:07 +0100 Subject: [PATCH 02/13] Fix the build when TF-PSA-Crypto only has a little MLDSA When TF-PSA-Crypto has pure ML-DSA, we need to handle it in the test driver for signatures. But we must not try to reference ML-DSA identifiers in TF-PSA-Crypto branches where they don't exist yet, even though the compilation option already exists (which notably includes the TF-PSA-Crypto 1.1.0 release). Signed-off-by: Gilles Peskine --- tests/src/drivers/test_driver_signature.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/src/drivers/test_driver_signature.c b/tests/src/drivers/test_driver_signature.c index 010cdfcf46..6ff2ad9c28 100644 --- a/tests/src/drivers/test_driver_signature.c +++ b/tests/src/drivers/test_driver_signature.c @@ -222,7 +222,11 @@ psa_status_t mbedtls_test_transparent_signature_sign_message( return PSA_SUCCESS; } -#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) + /* In TF-PSA-Crypto 1.1.0, TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED and + * psa_crypto_mldsa.h already exist, but there is no driver dispatch for + * ML-DSA and PSA_ALG_IS_ML_DSA doesn't exist yet. After that, we need + * to worry about pure ML-DSA. */ +#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) && defined(PSA_ALG_IS_ML_DSA) /* Pure ML-DSA is not a sign-the-hash algorithm. At the moment, this * function only knows how to deal with sign-the-hash algorithms. * So give up and let the next driver in the chain handle the algorithm. @@ -303,7 +307,11 @@ psa_status_t mbedtls_test_transparent_signature_verify_message( return mbedtls_test_driver_signature_verify_hooks.forced_status; } -#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) + /* In TF-PSA-Crypto 1.1.0, TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED and + * psa_crypto_mldsa.h already exist, but there is no driver dispatch for + * ML-DSA and PSA_ALG_IS_ML_DSA doesn't exist yet. After that, we need + * to worry about pure ML-DSA. */ +#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) && defined(PSA_ALG_IS_ML_DSA) /* Pure ML-DSA is not a sign-the-hash algorithm. At the moment, this * function only knows how to deal with sign-the-hash algorithms. * So give up and let the next driver in the chain handle the algorithm. From a0e5850d6112e29de8587612f6415b00bd129d35 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Thu, 26 Mar 2026 14:00:45 +0100 Subject: [PATCH 03/13] Fix the Mbed TLS build when TF-PSA-Crypto only has a little MLDSA When building Mbed TLS with test drivers, "psa_crypto_mldsa.h" is not on the include path. Rather than get it on, which seems complicated and is not desirable in the long term, arrange to do without this header. We just need to define the macro PSA_ALG_IS_ML_DSA, and its behavior is defined by the PSA Crypto API specification so pretty much set in stone. Signed-off-by: Gilles Peskine --- tests/src/drivers/test_driver_signature.c | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/tests/src/drivers/test_driver_signature.c b/tests/src/drivers/test_driver_signature.c index 6ff2ad9c28..7647c9c651 100644 --- a/tests/src/drivers/test_driver_signature.c +++ b/tests/src/drivers/test_driver_signature.c @@ -39,13 +39,28 @@ #include LIBTESTDRIVER1_PSA_DRIVER_INTERNAL_HEADER(psa_crypto_rsa.h) #endif -#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) -/* For PSA_ALG_IS_ML_DSA. Including this internal header will no longer - * be needed once we add the ML-DSA macro definitions to the public - * headers. - * https://github.com/Mbed-TLS/TF-PSA-Crypto/issues/726 +/* This file is part of the framework and needs to be compatible with all + * maintained branches of Mbed TLS and TF-PSA-Crypto. + * + * - Until shortly before TF-PSA-Crypto 1.1.0, ML-DSA does not exist at all. + * - In TF-PSA-Crypto 1.1.0, TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED exists, but + * there is no driver dispatch for it yet, so this driver doesn't need to + * worry about ML-DSA. + * - Shortly after TF-PSA-Crypto 1.1.0, in + * https://github.com/Mbed-TLS/TF-PSA-Crypto/pull/700, we introduced + * driver dispatch for ML-DSA, but the macro PSA_ALG_IS_ML_DSA is not + * in the API yet, only in a private header. Including this private header + * is a pain due to how our various build scripts set up include paths, so + * we don't do it. Instead, define PSA_ALG_IS_ML_DSA manually: it's the + * only thing we need. + * - Later we will add ML-DSA to the API, including the definition of + * PSA_ALG_IS_ML_DSA. After that we may also add driver dispatch testing + * for ML-DSA. */ -#include "psa_crypto_mldsa.h" +#if !defined(PSA_ALG_IS_ML_DSA) +/* Pure ML-DSA (hedged or deterministic) */ +#define PSA_ALG_IS_ML_DSA(alg) \ + ((alg) == 0x06004400u || (alg) == 0x06004500u) #endif #include @@ -222,11 +237,7 @@ psa_status_t mbedtls_test_transparent_signature_sign_message( return PSA_SUCCESS; } - /* In TF-PSA-Crypto 1.1.0, TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED and - * psa_crypto_mldsa.h already exist, but there is no driver dispatch for - * ML-DSA and PSA_ALG_IS_ML_DSA doesn't exist yet. After that, we need - * to worry about pure ML-DSA. */ -#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) && defined(PSA_ALG_IS_ML_DSA) +#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) /* Pure ML-DSA is not a sign-the-hash algorithm. At the moment, this * function only knows how to deal with sign-the-hash algorithms. * So give up and let the next driver in the chain handle the algorithm. @@ -307,11 +318,7 @@ psa_status_t mbedtls_test_transparent_signature_verify_message( return mbedtls_test_driver_signature_verify_hooks.forced_status; } - /* In TF-PSA-Crypto 1.1.0, TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED and - * psa_crypto_mldsa.h already exist, but there is no driver dispatch for - * ML-DSA and PSA_ALG_IS_ML_DSA doesn't exist yet. After that, we need - * to worry about pure ML-DSA. */ -#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) && defined(PSA_ALG_IS_ML_DSA) +#if defined(TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) /* Pure ML-DSA is not a sign-the-hash algorithm. At the moment, this * function only knows how to deal with sign-the-hash algorithms. * So give up and let the next driver in the chain handle the algorithm. From 7f537471bd11966726fbe51a3d75e8b23d824ca1 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 13:15:44 +0200 Subject: [PATCH 04/13] Clean up overly complicated typing in read_file_lines The `read_file_lines` context manager supported either text or binary streams, based on a parameter passed to the constructor. But the type annotation on the iterator claimed that all lines were text. The version of mypy that we use on the CI was happy with that, but modern versions are not. The advantage of `read_file_lines` over built-in functions is better tracking of line numbers. We never took advantage of this in our code with binary streams. Change the one place where `read_file_lines` was used with a binary stream to use built-in functions instead, and specialize `read_file_lines` to text streams. This fixes a legitimate complaint of modern mypy on `macro_collectory.py`. There was no runtime bug: the behavior was correct, only the type annotations were wrong. Signed-off-by: Gilles Peskine --- scripts/mbedtls_framework/macro_collector.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/mbedtls_framework/macro_collector.py b/scripts/mbedtls_framework/macro_collector.py index 515b9191f7..ccc9a9691a 100644 --- a/scripts/mbedtls_framework/macro_collector.py +++ b/scripts/mbedtls_framework/macro_collector.py @@ -37,14 +37,13 @@ class read_file_lines: except that if process(line) raises an exception, then the read_file_lines snippet annotates the exception with the file name and line number. """ - def __init__(self, filename: str, binary: bool = False) -> None: + def __init__(self, filename: str) -> None: self.filename = filename self.file = None #type: Optional[IO[str]] self.line_number = 'entry' #type: Union[int, str] self.generator = None #type: Optional[Iterable[Tuple[int, str]]] - self.binary = binary def __enter__(self) -> 'read_file_lines': - self.file = open(self.filename, 'rb' if self.binary else 'r') + self.file = open(self.filename) self.generator = enumerate(self.file) return self def __iter__(self) -> Iterator[str]: @@ -517,10 +516,10 @@ def parse_header_line(self, line: str) -> None: _nonascii_re = re.compile(rb'[^\x00-\x7f]+') #type: Pattern def parse_header(self, filename: str) -> None: """Parse a C header file, looking for "#define PSA_xxx".""" - with read_file_lines(filename, binary=True) as lines: - for line in lines: - line = re.sub(self._nonascii_re, rb'', line).decode('ascii') - self.parse_header_line(line) + with open(filename, 'rb') as input_: + for line in input_: + line = re.sub(self._nonascii_re, rb'', line) + self.parse_header_line(line.decode('ascii')) _macro_identifier_re = re.compile(r'[A-Z]\w+') def generate_undeclared_names(self, expr: str) -> Iterable[str]: From 72f178bdbd1457a5470acd3885d8c9fa81e39bc2 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 13:21:45 +0200 Subject: [PATCH 05/13] Move most of generate_mldsa_tests.py into a module We are moving the command line entry point to the consuming branch. The move will be completed when the consuming branch no longer needs the script to exist in the framework. https://github.com/Mbed-TLS/mbedtls-framework/issues/294 Signed-off-by: Gilles Peskine --- util/generate_mldsa_tests.py | 192 +----------------- .../mldsa_test_generator.py | 190 +++++++++++++++++ 2 files changed, 196 insertions(+), 186 deletions(-) create mode 100644 util/mbedtls_maintainer/mldsa_test_generator.py diff --git a/util/generate_mldsa_tests.py b/util/generate_mldsa_tests.py index 4cfe3e7634..15854d31d1 100755 --- a/util/generate_mldsa_tests.py +++ b/util/generate_mldsa_tests.py @@ -1,203 +1,23 @@ #!/usr/bin/env python3 """Generate ML-DSA test cases. -""" -# Copyright The Mbed TLS Contributors -# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later +This is a transitional script that does not handle different feature sets +in different states of TF-PSA-Crypto. The live version of this script +is `scripts/maintainer/generate_mldsa_tests.py` in TF-PSA-Crypto. +""" import sys -from typing import Iterable, List, Optional - -# pip install dilithium-py -import dilithium_py.ml_dsa #type: ignore import scripts_path # pylint: disable=unused-import -from mbedtls_framework import test_case from mbedtls_framework import test_data_generation - -# ML_DSA instances for pure ML-DSA -PURE = { - #44: dilithium_py.ml_dsa.ML_DSA_44, - #65: dilithium_py.ml_dsa.ML_DSA_65, - 87: dilithium_py.ml_dsa.ML_DSA_87, -} - -# ML_DSA instances for HashML-DSA -HASH = { - #44: dilithium_py.ml_dsa.HASH_ML_DSA_44_WITH_SHA512, - #65: dilithium_py.ml_dsa.HASH_ML_DSA_65_WITH_SHA512, - 87: dilithium_py.ml_dsa.HASH_ML_DSA_87_WITH_SHA512, -} - -# Seeds (i.e. private keys) to test with. -SEEDS = [ - b'There was once upon a time a ...', - b'\x00' * 32, -] - -class Key: - """An MLDSA key pair.""" - #pylint: disable=too-few-public-methods - - def __init__(self, kl: int, seed: bytes) -> None: - self.kl = kl #pylint: disable=invalid-name - self.seed = seed - self.public, self.secret = PURE[kl]._keygen_internal(seed) - - def sign_message(self, message: bytes, deterministic: bool) -> bytes: - PURE[self.kl].set_drbg_seed(bytes(48)) - return PURE[self.kl].sign(self.secret, message, - deterministic=deterministic) - -# Key pairs to test with. -KEYS = {kl: [Key(kl, seed) for seed in SEEDS] - for kl in sorted(PURE.keys())} - -# Input messages to test with. -MESSAGES = [ - (b'This is a test', ''), - (b'', 'empty message'), - (b'\x00', '"\\x00"'), - (b'\x01', '"\\x01"'), - (b'ACBDEFGHIJ' * 100, '1000B'), -] - - -class API: - """Abstract base class for the interface of the test functions.""" - - @classmethod - def function(cls, func: str, kl: int) -> str: - raise NotImplementedError - - @classmethod - def metadata_arguments(cls, - kl: int, - pair: bool, - deterministic: Optional[bool]) -> List[str]: - raise NotImplementedError - - @classmethod - def final_arguments(cls) -> List[str]: - return [] - - @classmethod - def secret_is_seed(cls) -> bool: - return True - - -class PQCPAPI(API): - """Test mldsa-native entry points.""" - - @classmethod - def function(cls, func: str, kl: int) -> str: - return f'{func}_{kl}' - - @classmethod - def metadata_arguments(cls, - _kl: int, - _pair: bool, - _deterministic: Optional[bool]) -> List[str]: - return [] - - @classmethod - def secret_is_seed(cls) -> bool: - return False - - -def one_mldsa_key_pair_from_seed(key: Key, - descr: str) -> test_case.TestCase: - """Construct one test case for mldsa-native keypair_internal().""" - tc = test_case.TestCase() - tc.set_function(f'key_pair_from_seed_{key.kl}') - tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments([ - test_case.hex_string(key.seed), - test_case.hex_string(key.secret), - test_case.hex_string(key.public), - ]) - tc.set_description(f'MLDSA-{key.kl} key pair from seed {descr}') - return tc - -def gen_pqcp_key_management(kl: int) -> Iterable[test_case.TestCase]: - """Generate test cases for mldsa-native keypair_internal().""" - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_key_pair_from_seed(key, f'key#{i}') - -def one_mldsa_sign_deterministic_pure(api: API, - key: Key, - message: bytes, - descr: str) -> test_case.TestCase: - """Construct one test case for deterministic signature.""" - signature = key.sign_message(message, deterministic=True) - tc = test_case.TestCase() - tc.set_function(api.function('sign_deterministic_pure', key.kl)) - tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments(api.metadata_arguments(key.kl, True, True) + [ - test_case.hex_string(key.seed if api.secret_is_seed() else key.secret), - test_case.hex_string(message), - test_case.hex_string(signature), - ] + api.final_arguments()) - tc.set_description(f'MLDSA-{key.kl} sign deterministic {descr}') - return tc - -def one_mldsa_verify_pure(api: API, - key: Key, - message: bytes, - deterministic: bool, - descr: str) -> test_case.TestCase: - """Construct one test case for verification. - - When deterministic is true, the test case is a deterministic signature. - When deterministic is false, the test case is some other valid signature. - """ - signature = key.sign_message(message, deterministic=deterministic) - tc = test_case.TestCase() - tc.set_function(api.function('verify_pure', key.kl)) - tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments(api.metadata_arguments(key.kl, False, True) + [ - test_case.hex_string(key.public), - test_case.hex_string(message), - test_case.hex_string(signature), - ] + api.final_arguments()) - variant = "deterministic" if deterministic else "randomized" - tc.set_description(f'MLDSA-{key.kl} verify {variant} {descr}') - return tc - -def gen_mldsa_pure(api: API, kl: int) -> Iterable[test_case.TestCase]: - """Generate all test cases for pure ML-DSA signature and verification.""" - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_sign_deterministic_pure(api, key, MESSAGES[0][0], - f'key#{i}') - for message, descr in MESSAGES[1:]: - yield one_mldsa_sign_deterministic_pure(api, KEYS[kl][0], message, - f'key#1 {descr}') - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], True, - f'key#{i}') - for message, descr in MESSAGES[1:]: - yield one_mldsa_verify_pure(api, KEYS[kl][0], message, True, - f'key#1 {descr}') - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], False, - f'key#{i}') - for message, descr in MESSAGES[1:]: - yield one_mldsa_verify_pure(api, KEYS[kl][0], message, False, - f'key#1 {descr}') - -def gen_pqcp_mldsa_all() -> Iterable[test_case.TestCase]: - """Generate all test cases for mldsa-native.""" - api = PQCPAPI() - for kl in sorted(KEYS.keys()): - yield from gen_pqcp_key_management(kl) - yield from gen_mldsa_pure(api, kl) +from mbedtls_maintainer import mldsa_test_generator class MLDSATestGenerator(test_data_generation.TestGenerator): """Generate test cases for ML-DSA.""" def __init__(self, settings) -> None: self.targets = { - 'test_suite_pqcp_mldsa.dilithium_py': gen_pqcp_mldsa_all, + 'test_suite_pqcp_mldsa.dilithium_py': mldsa_test_generator.gen_pqcp_mldsa_all, } super().__init__(settings) diff --git a/util/mbedtls_maintainer/mldsa_test_generator.py b/util/mbedtls_maintainer/mldsa_test_generator.py new file mode 100644 index 0000000000..384fd8de6b --- /dev/null +++ b/util/mbedtls_maintainer/mldsa_test_generator.py @@ -0,0 +1,190 @@ +"""Generate ML-DSA test cases. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +from typing import Iterable, List, Optional + +# pip install dilithium-py +import dilithium_py.ml_dsa #type: ignore + +import scripts_path # pylint: disable=unused-import +from mbedtls_framework import test_case + +# ML_DSA instances for pure ML-DSA +PURE = { + #44: dilithium_py.ml_dsa.ML_DSA_44, + #65: dilithium_py.ml_dsa.ML_DSA_65, + 87: dilithium_py.ml_dsa.ML_DSA_87, +} + +# ML_DSA instances for HashML-DSA +HASH = { + #44: dilithium_py.ml_dsa.HASH_ML_DSA_44_WITH_SHA512, + #65: dilithium_py.ml_dsa.HASH_ML_DSA_65_WITH_SHA512, + 87: dilithium_py.ml_dsa.HASH_ML_DSA_87_WITH_SHA512, +} + +# Seeds (i.e. private keys) to test with. +SEEDS = [ + b'There was once upon a time a ...', + b'\x00' * 32, +] + +class Key: + """An MLDSA key pair.""" + #pylint: disable=too-few-public-methods + + def __init__(self, kl: int, seed: bytes) -> None: + self.kl = kl #pylint: disable=invalid-name + self.seed = seed + self.public, self.secret = PURE[kl]._keygen_internal(seed) + + def sign_message(self, message: bytes, deterministic: bool) -> bytes: + PURE[self.kl].set_drbg_seed(bytes(48)) + return PURE[self.kl].sign(self.secret, message, + deterministic=deterministic) + +# Key pairs to test with. +KEYS = {kl: [Key(kl, seed) for seed in SEEDS] + for kl in sorted(PURE.keys())} + +# Input messages to test with. +MESSAGES = [ + (b'This is a test', ''), + (b'', 'empty message'), + (b'\x00', '"\\x00"'), + (b'\x01', '"\\x01"'), + (b'ACBDEFGHIJ' * 100, '1000B'), +] + + +class API: + """Abstract base class for the interface of the test functions.""" + + @classmethod + def function(cls, func: str, kl: int) -> str: + raise NotImplementedError + + @classmethod + def metadata_arguments(cls, + kl: int, + pair: bool, + deterministic: Optional[bool]) -> List[str]: + raise NotImplementedError + + @classmethod + def final_arguments(cls) -> List[str]: + return [] + + @classmethod + def secret_is_seed(cls) -> bool: + return True + + +class PQCPAPI(API): + """Test mldsa-native entry points.""" + + @classmethod + def function(cls, func: str, kl: int) -> str: + return f'{func}_{kl}' + + @classmethod + def metadata_arguments(cls, + _kl: int, + _pair: bool, + _deterministic: Optional[bool]) -> List[str]: + return [] + + @classmethod + def secret_is_seed(cls) -> bool: + return False + + +def one_mldsa_key_pair_from_seed(key: Key, + descr: str) -> test_case.TestCase: + """Construct one test case for mldsa-native keypair_internal().""" + tc = test_case.TestCase() + tc.set_function(f'key_pair_from_seed_{key.kl}') + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments([ + test_case.hex_string(key.seed), + test_case.hex_string(key.secret), + test_case.hex_string(key.public), + ]) + tc.set_description(f'MLDSA-{key.kl} key pair from seed {descr}') + return tc + +def gen_pqcp_key_management(kl: int) -> Iterable[test_case.TestCase]: + """Generate test cases for mldsa-native keypair_internal().""" + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_key_pair_from_seed(key, f'key#{i}') + +def one_mldsa_sign_deterministic_pure(api: API, + key: Key, + message: bytes, + descr: str) -> test_case.TestCase: + """Construct one test case for deterministic signature.""" + signature = key.sign_message(message, deterministic=True) + tc = test_case.TestCase() + tc.set_function(api.function('sign_deterministic_pure', key.kl)) + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments(api.metadata_arguments(key.kl, True, True) + [ + test_case.hex_string(key.seed if api.secret_is_seed() else key.secret), + test_case.hex_string(message), + test_case.hex_string(signature), + ] + api.final_arguments()) + tc.set_description(f'MLDSA-{key.kl} sign deterministic {descr}') + return tc + +def one_mldsa_verify_pure(api: API, + key: Key, + message: bytes, + deterministic: bool, + descr: str) -> test_case.TestCase: + """Construct one test case for verification. + + When deterministic is true, the test case is a deterministic signature. + When deterministic is false, the test case is some other valid signature. + """ + signature = key.sign_message(message, deterministic=deterministic) + tc = test_case.TestCase() + tc.set_function(api.function('verify_pure', key.kl)) + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments(api.metadata_arguments(key.kl, False, True) + [ + test_case.hex_string(key.public), + test_case.hex_string(message), + test_case.hex_string(signature), + ] + api.final_arguments()) + variant = "deterministic" if deterministic else "randomized" + tc.set_description(f'MLDSA-{key.kl} verify {variant} {descr}') + return tc + +def gen_mldsa_pure(api: API, kl: int) -> Iterable[test_case.TestCase]: + """Generate all test cases for pure ML-DSA signature and verification.""" + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_sign_deterministic_pure(api, key, MESSAGES[0][0], + f'key#{i}') + for message, descr in MESSAGES[1:]: + yield one_mldsa_sign_deterministic_pure(api, KEYS[kl][0], message, + f'key#1 {descr}') + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], True, + f'key#{i}') + for message, descr in MESSAGES[1:]: + yield one_mldsa_verify_pure(api, KEYS[kl][0], message, True, + f'key#1 {descr}') + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], False, + f'key#{i}') + for message, descr in MESSAGES[1:]: + yield one_mldsa_verify_pure(api, KEYS[kl][0], message, False, + f'key#1 {descr}') + +def gen_pqcp_mldsa_all() -> Iterable[test_case.TestCase]: + """Generate all test cases for mldsa-native.""" + api = PQCPAPI() + for kl in sorted(KEYS.keys()): + yield from gen_pqcp_key_management(kl) + yield from gen_mldsa_pure(api, kl) From 31bae441f69cf79fa8276a1fdc431013cab408ea Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 13:44:53 +0200 Subject: [PATCH 06/13] mldsa_test_generator refactor: preliminary rearrangement Move code around, to make subsequent diffs easier to review. Signed-off-by: Gilles Peskine --- .../mldsa_test_generator.py | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/util/mbedtls_maintainer/mldsa_test_generator.py b/util/mbedtls_maintainer/mldsa_test_generator.py index 384fd8de6b..1b2f63aec8 100644 --- a/util/mbedtls_maintainer/mldsa_test_generator.py +++ b/util/mbedtls_maintainer/mldsa_test_generator.py @@ -82,45 +82,6 @@ def final_arguments(cls) -> List[str]: def secret_is_seed(cls) -> bool: return True - -class PQCPAPI(API): - """Test mldsa-native entry points.""" - - @classmethod - def function(cls, func: str, kl: int) -> str: - return f'{func}_{kl}' - - @classmethod - def metadata_arguments(cls, - _kl: int, - _pair: bool, - _deterministic: Optional[bool]) -> List[str]: - return [] - - @classmethod - def secret_is_seed(cls) -> bool: - return False - - -def one_mldsa_key_pair_from_seed(key: Key, - descr: str) -> test_case.TestCase: - """Construct one test case for mldsa-native keypair_internal().""" - tc = test_case.TestCase() - tc.set_function(f'key_pair_from_seed_{key.kl}') - tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments([ - test_case.hex_string(key.seed), - test_case.hex_string(key.secret), - test_case.hex_string(key.public), - ]) - tc.set_description(f'MLDSA-{key.kl} key pair from seed {descr}') - return tc - -def gen_pqcp_key_management(kl: int) -> Iterable[test_case.TestCase]: - """Generate test cases for mldsa-native keypair_internal().""" - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_key_pair_from_seed(key, f'key#{i}') - def one_mldsa_sign_deterministic_pure(api: API, key: Key, message: bytes, @@ -182,6 +143,44 @@ def gen_mldsa_pure(api: API, kl: int) -> Iterable[test_case.TestCase]: yield one_mldsa_verify_pure(api, KEYS[kl][0], message, False, f'key#1 {descr}') +class PQCPAPI(API): + """Test mldsa-native entry points.""" + + @classmethod + def function(cls, func: str, kl: int) -> str: + return f'{func}_{kl}' + + @classmethod + def metadata_arguments(cls, + _kl: int, + _pair: bool, + _deterministic: Optional[bool]) -> List[str]: + return [] + + @classmethod + def secret_is_seed(cls) -> bool: + return False + + +def one_mldsa_key_pair_from_seed(key: Key, + descr: str) -> test_case.TestCase: + """Construct one test case for mldsa-native keypair_internal().""" + tc = test_case.TestCase() + tc.set_function(f'key_pair_from_seed_{key.kl}') + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments([ + test_case.hex_string(key.seed), + test_case.hex_string(key.secret), + test_case.hex_string(key.public), + ]) + tc.set_description(f'MLDSA-{key.kl} key pair from seed {descr}') + return tc + +def gen_pqcp_key_management(kl: int) -> Iterable[test_case.TestCase]: + """Generate test cases for mldsa-native keypair_internal().""" + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_key_pair_from_seed(key, f'key#{i}') + def gen_pqcp_mldsa_all() -> Iterable[test_case.TestCase]: """Generate all test cases for mldsa-native.""" api = PQCPAPI() From 38d9d83ae7c406db51f88d21f4419dd4cc4ee404 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 13:41:42 +0200 Subject: [PATCH 07/13] mldsa_test_generator refactor: extend API class to Generator Turn functions that generate test cases into methods of the generator class. Functions that used to take an API argument are now implemented in the generic class, and API-specific functions are now methods of the corresponding API-specific concrete class. Signed-off-by: Gilles Peskine --- .../mldsa_test_generator.py | 184 ++++++++++-------- 1 file changed, 99 insertions(+), 85 deletions(-) diff --git a/util/mbedtls_maintainer/mldsa_test_generator.py b/util/mbedtls_maintainer/mldsa_test_generator.py index 1b2f63aec8..e75b2532ec 100644 --- a/util/mbedtls_maintainer/mldsa_test_generator.py +++ b/util/mbedtls_maintainer/mldsa_test_generator.py @@ -60,8 +60,17 @@ def sign_message(self, message: bytes, deterministic: bool) -> bytes: ] -class API: - """Abstract base class for the interface of the test functions.""" +def one_mldsa_sign_deterministic_pure(api, *args): + return api.one_mldsa_sign_deterministic_pure(*args) + +def one_mldsa_verify_pure(api, *args): + return api.one_mldsa_verify_pure(*args) + +def gen_mldsa_pure(api, *args): + return api.gen_mldsa_pure(*args) + +class Generator: + """Abstract base class to generate tests for one API.""" @classmethod def function(cls, func: str, kl: int) -> str: @@ -82,68 +91,72 @@ def final_arguments(cls) -> List[str]: def secret_is_seed(cls) -> bool: return True -def one_mldsa_sign_deterministic_pure(api: API, - key: Key, - message: bytes, - descr: str) -> test_case.TestCase: - """Construct one test case for deterministic signature.""" - signature = key.sign_message(message, deterministic=True) - tc = test_case.TestCase() - tc.set_function(api.function('sign_deterministic_pure', key.kl)) - tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments(api.metadata_arguments(key.kl, True, True) + [ - test_case.hex_string(key.seed if api.secret_is_seed() else key.secret), - test_case.hex_string(message), - test_case.hex_string(signature), - ] + api.final_arguments()) - tc.set_description(f'MLDSA-{key.kl} sign deterministic {descr}') - return tc - -def one_mldsa_verify_pure(api: API, - key: Key, - message: bytes, - deterministic: bool, - descr: str) -> test_case.TestCase: - """Construct one test case for verification. - - When deterministic is true, the test case is a deterministic signature. - When deterministic is false, the test case is some other valid signature. - """ - signature = key.sign_message(message, deterministic=deterministic) - tc = test_case.TestCase() - tc.set_function(api.function('verify_pure', key.kl)) - tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments(api.metadata_arguments(key.kl, False, True) + [ - test_case.hex_string(key.public), - test_case.hex_string(message), - test_case.hex_string(signature), - ] + api.final_arguments()) - variant = "deterministic" if deterministic else "randomized" - tc.set_description(f'MLDSA-{key.kl} verify {variant} {descr}') - return tc - -def gen_mldsa_pure(api: API, kl: int) -> Iterable[test_case.TestCase]: - """Generate all test cases for pure ML-DSA signature and verification.""" - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_sign_deterministic_pure(api, key, MESSAGES[0][0], - f'key#{i}') - for message, descr in MESSAGES[1:]: - yield one_mldsa_sign_deterministic_pure(api, KEYS[kl][0], message, - f'key#1 {descr}') - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], True, - f'key#{i}') - for message, descr in MESSAGES[1:]: - yield one_mldsa_verify_pure(api, KEYS[kl][0], message, True, - f'key#1 {descr}') - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], False, - f'key#{i}') - for message, descr in MESSAGES[1:]: - yield one_mldsa_verify_pure(api, KEYS[kl][0], message, False, - f'key#1 {descr}') - -class PQCPAPI(API): + def one_mldsa_sign_deterministic_pure(self, + key: Key, + message: bytes, + descr: str) -> test_case.TestCase: + """Construct one test case for deterministic signature.""" + api = self + signature = key.sign_message(message, deterministic=True) + tc = test_case.TestCase() + tc.set_function(api.function('sign_deterministic_pure', key.kl)) + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments(api.metadata_arguments(key.kl, True, True) + [ + test_case.hex_string(key.seed if api.secret_is_seed() else key.secret), + test_case.hex_string(message), + test_case.hex_string(signature), + ] + api.final_arguments()) + tc.set_description(f'MLDSA-{key.kl} sign deterministic {descr}') + return tc + + def one_mldsa_verify_pure(self, + key: Key, + message: bytes, + deterministic: bool, + descr: str) -> test_case.TestCase: + """Construct one test case for verification. + + When deterministic is true, the test case is a deterministic signature. + When deterministic is false, the test case is some other valid signature. + """ + api = self + signature = key.sign_message(message, deterministic=deterministic) + tc = test_case.TestCase() + tc.set_function(api.function('verify_pure', key.kl)) + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments(api.metadata_arguments(key.kl, False, True) + [ + test_case.hex_string(key.public), + test_case.hex_string(message), + test_case.hex_string(signature), + ] + api.final_arguments()) + variant = "deterministic" if deterministic else "randomized" + tc.set_description(f'MLDSA-{key.kl} verify {variant} {descr}') + return tc + + def gen_mldsa_pure(self, kl: int) -> Iterable[test_case.TestCase]: + """Generate all test cases for pure ML-DSA signature and verification.""" + api = self + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_sign_deterministic_pure(api, key, MESSAGES[0][0], + f'key#{i}') + for message, descr in MESSAGES[1:]: + yield one_mldsa_sign_deterministic_pure(api, KEYS[kl][0], message, + f'key#1 {descr}') + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], True, + f'key#{i}') + for message, descr in MESSAGES[1:]: + yield one_mldsa_verify_pure(api, KEYS[kl][0], message, True, + f'key#1 {descr}') + for i, key in enumerate(KEYS[kl], 1): + yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], False, + f'key#{i}') + for message, descr in MESSAGES[1:]: + yield one_mldsa_verify_pure(api, KEYS[kl][0], message, False, + f'key#1 {descr}') + + +class PQCPGenerator(Generator): """Test mldsa-native entry points.""" @classmethod @@ -161,29 +174,30 @@ def metadata_arguments(cls, def secret_is_seed(cls) -> bool: return False + @staticmethod + def one_mldsa_key_pair_from_seed(key: Key, + descr: str) -> test_case.TestCase: + """Construct one test case for mldsa-native keypair_internal().""" + tc = test_case.TestCase() + tc.set_function(f'key_pair_from_seed_{key.kl}') + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments([ + test_case.hex_string(key.seed), + test_case.hex_string(key.secret), + test_case.hex_string(key.public), + ]) + tc.set_description(f'MLDSA-{key.kl} key pair from seed {descr}') + return tc + + def gen_pqcp_key_management(self, kl: int) -> Iterable[test_case.TestCase]: + """Generate test cases for mldsa-native keypair_internal().""" + for i, key in enumerate(KEYS[kl], 1): + yield self.one_mldsa_key_pair_from_seed(key, f'key#{i}') -def one_mldsa_key_pair_from_seed(key: Key, - descr: str) -> test_case.TestCase: - """Construct one test case for mldsa-native keypair_internal().""" - tc = test_case.TestCase() - tc.set_function(f'key_pair_from_seed_{key.kl}') - tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments([ - test_case.hex_string(key.seed), - test_case.hex_string(key.secret), - test_case.hex_string(key.public), - ]) - tc.set_description(f'MLDSA-{key.kl} key pair from seed {descr}') - return tc - -def gen_pqcp_key_management(kl: int) -> Iterable[test_case.TestCase]: - """Generate test cases for mldsa-native keypair_internal().""" - for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_key_pair_from_seed(key, f'key#{i}') def gen_pqcp_mldsa_all() -> Iterable[test_case.TestCase]: """Generate all test cases for mldsa-native.""" - api = PQCPAPI() + generator = PQCPGenerator() for kl in sorted(KEYS.keys()): - yield from gen_pqcp_key_management(kl) - yield from gen_mldsa_pure(api, kl) + yield from generator.gen_pqcp_key_management(kl) + yield from gen_mldsa_pure(generator, kl) From a53c51425dc490e720cb6ec2f8990f49ae39d48b Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 13:50:36 +0200 Subject: [PATCH 08/13] mldsa_test_generator refactor: change function calls to method calls Signed-off-by: Gilles Peskine --- .../mldsa_test_generator.py | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/util/mbedtls_maintainer/mldsa_test_generator.py b/util/mbedtls_maintainer/mldsa_test_generator.py index e75b2532ec..24226d77cb 100644 --- a/util/mbedtls_maintainer/mldsa_test_generator.py +++ b/util/mbedtls_maintainer/mldsa_test_generator.py @@ -60,15 +60,6 @@ def sign_message(self, message: bytes, deterministic: bool) -> bytes: ] -def one_mldsa_sign_deterministic_pure(api, *args): - return api.one_mldsa_sign_deterministic_pure(*args) - -def one_mldsa_verify_pure(api, *args): - return api.one_mldsa_verify_pure(*args) - -def gen_mldsa_pure(api, *args): - return api.gen_mldsa_pure(*args) - class Generator: """Abstract base class to generate tests for one API.""" @@ -96,16 +87,15 @@ def one_mldsa_sign_deterministic_pure(self, message: bytes, descr: str) -> test_case.TestCase: """Construct one test case for deterministic signature.""" - api = self signature = key.sign_message(message, deterministic=True) tc = test_case.TestCase() - tc.set_function(api.function('sign_deterministic_pure', key.kl)) + tc.set_function(self.function('sign_deterministic_pure', key.kl)) tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments(api.metadata_arguments(key.kl, True, True) + [ - test_case.hex_string(key.seed if api.secret_is_seed() else key.secret), + tc.set_arguments(self.metadata_arguments(key.kl, True, True) + [ + test_case.hex_string(key.seed if self.secret_is_seed() else key.secret), test_case.hex_string(message), test_case.hex_string(signature), - ] + api.final_arguments()) + ] + self.final_arguments()) tc.set_description(f'MLDSA-{key.kl} sign deterministic {descr}') return tc @@ -119,41 +109,39 @@ def one_mldsa_verify_pure(self, When deterministic is true, the test case is a deterministic signature. When deterministic is false, the test case is some other valid signature. """ - api = self signature = key.sign_message(message, deterministic=deterministic) tc = test_case.TestCase() - tc.set_function(api.function('verify_pure', key.kl)) + tc.set_function(self.function('verify_pure', key.kl)) tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) - tc.set_arguments(api.metadata_arguments(key.kl, False, True) + [ + tc.set_arguments(self.metadata_arguments(key.kl, False, True) + [ test_case.hex_string(key.public), test_case.hex_string(message), test_case.hex_string(signature), - ] + api.final_arguments()) + ] + self.final_arguments()) variant = "deterministic" if deterministic else "randomized" tc.set_description(f'MLDSA-{key.kl} verify {variant} {descr}') return tc def gen_mldsa_pure(self, kl: int) -> Iterable[test_case.TestCase]: """Generate all test cases for pure ML-DSA signature and verification.""" - api = self for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_sign_deterministic_pure(api, key, MESSAGES[0][0], - f'key#{i}') + yield self.one_mldsa_sign_deterministic_pure(key, MESSAGES[0][0], + f'key#{i}') for message, descr in MESSAGES[1:]: - yield one_mldsa_sign_deterministic_pure(api, KEYS[kl][0], message, - f'key#1 {descr}') + yield self.one_mldsa_sign_deterministic_pure(KEYS[kl][0], message, + f'key#1 {descr}') for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], True, - f'key#{i}') + yield self.one_mldsa_verify_pure(key, MESSAGES[0][0], True, + f'key#{i}') for message, descr in MESSAGES[1:]: - yield one_mldsa_verify_pure(api, KEYS[kl][0], message, True, - f'key#1 {descr}') + yield self.one_mldsa_verify_pure(KEYS[kl][0], message, True, + f'key#1 {descr}') for i, key in enumerate(KEYS[kl], 1): - yield one_mldsa_verify_pure(api, key, MESSAGES[0][0], False, - f'key#{i}') + yield self.one_mldsa_verify_pure(key, MESSAGES[0][0], False, + f'key#{i}') for message, descr in MESSAGES[1:]: - yield one_mldsa_verify_pure(api, KEYS[kl][0], message, False, - f'key#1 {descr}') + yield self.one_mldsa_verify_pure(KEYS[kl][0], message, False, + f'key#1 {descr}') class PQCPGenerator(Generator): @@ -200,4 +188,4 @@ def gen_pqcp_mldsa_all() -> Iterable[test_case.TestCase]: generator = PQCPGenerator() for kl in sorted(KEYS.keys()): yield from generator.gen_pqcp_key_management(kl) - yield from gen_mldsa_pure(generator, kl) + yield from generator.gen_mldsa_pure(kl) From 85a47fc27763a765ad4040c885995b5cec095276 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 13:57:11 +0200 Subject: [PATCH 09/13] mldsa_test_generator refactor: new methods gen_key_management, gen_all Signed-off-by: Gilles Peskine --- .../mldsa_test_generator.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/util/mbedtls_maintainer/mldsa_test_generator.py b/util/mbedtls_maintainer/mldsa_test_generator.py index 24226d77cb..5b7bc90778 100644 --- a/util/mbedtls_maintainer/mldsa_test_generator.py +++ b/util/mbedtls_maintainer/mldsa_test_generator.py @@ -4,7 +4,7 @@ # Copyright The Mbed TLS Contributors # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later -from typing import Iterable, List, Optional +from typing import Iterator, List, Optional # pip install dilithium-py import dilithium_py.ml_dsa #type: ignore @@ -122,7 +122,7 @@ def one_mldsa_verify_pure(self, tc.set_description(f'MLDSA-{key.kl} verify {variant} {descr}') return tc - def gen_mldsa_pure(self, kl: int) -> Iterable[test_case.TestCase]: + def gen_mldsa_pure(self, kl: int) -> Iterator[test_case.TestCase]: """Generate all test cases for pure ML-DSA signature and verification.""" for i, key in enumerate(KEYS[kl], 1): yield self.one_mldsa_sign_deterministic_pure(key, MESSAGES[0][0], @@ -143,6 +143,16 @@ def gen_mldsa_pure(self, kl: int) -> Iterable[test_case.TestCase]: yield self.one_mldsa_verify_pure(KEYS[kl][0], message, False, f'key#1 {descr}') + def gen_key_management(self, kl: int) -> Iterator[test_case.TestCase]: + """Generate all key management test cases for the given parameter set.""" + raise NotImplementedError + + def gen_all(self) -> Iterator[test_case.TestCase]: + """Generate all the tests for this API.""" + for kl in sorted(KEYS.keys()): + yield from self.gen_key_management(kl) + yield from self.gen_mldsa_pure(kl) + class PQCPGenerator(Generator): """Test mldsa-native entry points.""" @@ -177,15 +187,13 @@ def one_mldsa_key_pair_from_seed(key: Key, tc.set_description(f'MLDSA-{key.kl} key pair from seed {descr}') return tc - def gen_pqcp_key_management(self, kl: int) -> Iterable[test_case.TestCase]: + def gen_key_management(self, kl: int) -> Iterator[test_case.TestCase]: """Generate test cases for mldsa-native keypair_internal().""" for i, key in enumerate(KEYS[kl], 1): yield self.one_mldsa_key_pair_from_seed(key, f'key#{i}') -def gen_pqcp_mldsa_all() -> Iterable[test_case.TestCase]: +def gen_pqcp_mldsa_all() -> Iterator[test_case.TestCase]: """Generate all test cases for mldsa-native.""" generator = PQCPGenerator() - for kl in sorted(KEYS.keys()): - yield from generator.gen_pqcp_key_management(kl) - yield from generator.gen_mldsa_pure(kl) + yield from generator.gen_all() From 6466dc62c31d382837de7d7d5d2e4201e86e24fb Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 14:38:15 +0200 Subject: [PATCH 10/13] Make mbedtls_maintainer a Python package Signed-off-by: Gilles Peskine --- util/mbedtls_maintainer/__init__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 util/mbedtls_maintainer/__init__.py diff --git a/util/mbedtls_maintainer/__init__.py b/util/mbedtls_maintainer/__init__.py new file mode 100644 index 0000000000..6a5ed79ad2 --- /dev/null +++ b/util/mbedtls_maintainer/__init__.py @@ -0,0 +1,3 @@ +# This file needs to exist to make mbedtls_maintainer a package. +# Among other things, this allows modules in this directory to make +# relative imports. From ab001120d6a1ef5246b2e87c3901634bc0ff37fa Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 14:38:34 +0200 Subject: [PATCH 11/13] Exclude maintainer scripts from Python checks Maintainer scripts may require a more recent Python than the version we currently use for Python checks (3.6). https://github.com/Mbed-TLS/mbedtls-framework/issues/293 Signed-off-by: Gilles Peskine --- scripts/check-python-files.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/check-python-files.sh b/scripts/check-python-files.sh index 39e005cf2e..aef7143ee0 100755 --- a/scripts/check-python-files.sh +++ b/scripts/check-python-files.sh @@ -55,6 +55,10 @@ elif [ "$1" = "--can-mypy" ]; then fi echo 'Running pylint ...' +# Exclude `maintainer` subdirectories, because they can contain code +# that does not work with the versions of pylint and mypy we use on the CI. +# https://github.com/Mbed-TLS/mbedtls-framework/issues/293 +# # When we move Python code between repositories, there is a transition # period during which code is duplicated between the old repository and # the new repository. @@ -64,7 +68,9 @@ echo 'Running pylint ...' # runs of pylint: one for the A files, and one for the others. # Remove exceptions below once the A file (or the moved code in the A file) # has been removed from all consuming branches. -find framework/scripts scripts tests/scripts -name '*.py' \( \ +find framework/scripts scripts tests/scripts \ + -name maintainer -prune -o \ + -name '*.py' \( \ ! -path scripts/abi_check.py \ ! -path scripts/code_size_compare.py \ ! -path scripts/ecp_comb_table.py \ @@ -87,7 +93,10 @@ $PYTHON -m mypy framework/scripts || { ret=1 } -$PYTHON -m mypy scripts tests/scripts || { +# Exclude `maintainer` subdirectories, because they can contain code +# that does not work with the versions of pylint and mypy we use on the CI. +# https://github.com/Mbed-TLS/mbedtls-framework/issues/293 +$PYTHON -m mypy --exclude maintainer scripts tests/scripts || { echo >&2 "mypy reported errors in the parent repository" ret=1 } From 4a58f90329e263df8c417ba0e047f27edd37b566 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 14:58:51 +0200 Subject: [PATCH 12/13] Generate MLDSA test cases for the driver entry points Signed-off-by: Gilles Peskine --- .../mldsa_test_generator.py | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/util/mbedtls_maintainer/mldsa_test_generator.py b/util/mbedtls_maintainer/mldsa_test_generator.py index 5b7bc90778..8227ff1e91 100644 --- a/util/mbedtls_maintainer/mldsa_test_generator.py +++ b/util/mbedtls_maintainer/mldsa_test_generator.py @@ -82,6 +82,19 @@ def final_arguments(cls) -> List[str]: def secret_is_seed(cls) -> bool: return True + def one_mldsa_public_key_from_seed(self, key: Key, + descr: str) -> test_case.TestCase: + """Construct one test case for driver export_public_key().""" + tc = test_case.TestCase() + tc.set_function('export_public_key') + tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) + tc.set_arguments(self.metadata_arguments(key.kl, True, None) + [ + test_case.hex_string(key.seed), + test_case.hex_string(key.public), + ] + self.final_arguments()) + tc.set_description(f'MLDSA-{key.kl} export public key from seed {descr}') + return tc + def one_mldsa_sign_deterministic_pure(self, key: Key, message: bytes, @@ -89,7 +102,7 @@ def one_mldsa_sign_deterministic_pure(self, """Construct one test case for deterministic signature.""" signature = key.sign_message(message, deterministic=True) tc = test_case.TestCase() - tc.set_function(self.function('sign_deterministic_pure', key.kl)) + tc.set_function(self.function('sign_message_deterministic', key.kl)) tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) tc.set_arguments(self.metadata_arguments(key.kl, True, True) + [ test_case.hex_string(key.seed if self.secret_is_seed() else key.secret), @@ -111,7 +124,7 @@ def one_mldsa_verify_pure(self, """ signature = key.sign_message(message, deterministic=deterministic) tc = test_case.TestCase() - tc.set_function(self.function('verify_pure', key.kl)) + tc.set_function(self.function('verify_message', key.kl)) tc.set_dependencies([f'TF_PSA_CRYPTO_PQCP_MLDSA_{key.kl}_ENABLED']) tc.set_arguments(self.metadata_arguments(key.kl, False, True) + [ test_case.hex_string(key.public), @@ -159,6 +172,10 @@ class PQCPGenerator(Generator): @classmethod def function(cls, func: str, kl: int) -> str: + if func == 'verify_message': + func = 'verify_pure' + elif func == 'sign_message_deterministic': + func = 'sign_deterministic_pure' return f'{func}_{kl}' @classmethod @@ -197,3 +214,38 @@ def gen_pqcp_mldsa_all() -> Iterator[test_case.TestCase]: """Generate all test cases for mldsa-native.""" generator = PQCPGenerator() yield from generator.gen_all() + + +class DriverGenerator(Generator): + """Test driver entry points.""" + + @classmethod + def function(cls, func: str, _kl: int) -> str: + if func == 'verify_message': + func = 'verify_pure' + elif func == 'sign_message_deterministic': + func = 'sign_deterministic_pure' + return func + + @classmethod + def metadata_arguments(cls, + kl: int, + pair: bool, + deterministic: Optional[bool]) -> List[str]: + arguments = [] + arguments.append('PSA_KEY_TYPE_ML_DSA_KEY_PAIR' if pair else + 'PSA_KEY_TYPE_ML_DSA_PUBLIC_KEY') + arguments.append(str(kl)) + if deterministic is not None: + arguments.append('PSA_ALG_DETERMINISTIC_ML_DSA' if deterministic else + 'PSA_ALG_ML_DSA') + return arguments + + @classmethod + def final_arguments(cls) -> List[str]: + return ['PSA_SUCCESS'] + + def gen_key_management(self, kl: int) -> Iterator[test_case.TestCase]: + """Generate test cases for driver export_public_key().""" + for i, key in enumerate(KEYS[kl], 1): + yield self.one_mldsa_public_key_from_seed(key, f'key#{i}') From dcf227c5b9b0f54294bb649b772b5a0c91ac724f Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 31 Mar 2026 14:59:14 +0200 Subject: [PATCH 13/13] Generate MLDSA test cases for the driver dispatch layer Signed-off-by: Gilles Peskine --- util/mbedtls_maintainer/mldsa_test_generator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/util/mbedtls_maintainer/mldsa_test_generator.py b/util/mbedtls_maintainer/mldsa_test_generator.py index 8227ff1e91..72b20ce359 100644 --- a/util/mbedtls_maintainer/mldsa_test_generator.py +++ b/util/mbedtls_maintainer/mldsa_test_generator.py @@ -249,3 +249,11 @@ def gen_key_management(self, kl: int) -> Iterator[test_case.TestCase]: """Generate test cases for driver export_public_key().""" for i, key in enumerate(KEYS[kl], 1): yield self.one_mldsa_public_key_from_seed(key, f'key#{i}') + + +class DispatchGenerator(DriverGenerator): + """Test the driver dispatch layer.""" + + @classmethod + def function(cls, func: str, _kl: int) -> str: + return func