Revision control

Copy as Markdown

Other Tools

"""
(C) 2026 Jack Lloyd
(C) 2026 René Meusel, Rohde & Schwarz Cybersecurity
Botan is released under the Simplified BSD License (see license.txt)
"""
import binascii
import unittest
import botan3 as botan
from .wycheproof import WycheproofTests, NullRNG
def _from_hex(value: str) -> bytes:
return binascii.unhexlify(value)
def _map_algorithm_to_mode(algorithm: str) -> str:
"""Map Wycheproof algorithm name to Botan ML-DSA mode name."""
mapping = {
"ML-DSA-44": "ML-DSA-4x4",
"ML-DSA-65": "ML-DSA-6x5",
"ML-DSA-87": "ML-DSA-8x7",
}
return mapping.get(algorithm, algorithm)
class TestMLDSA(WycheproofTests, unittest.TestCase):
def input_files(self) -> list[str]:
# TODO: enable *_noseed_*.json once support for expanded private keys is implemented
return [
"mldsa_44_sign_seed_test.json",
# "mldsa_44_sign_noseed_test.json",
"mldsa_44_verify_test.json",
"mldsa_65_sign_seed_test.json",
# "mldsa_65_sign_noseed_test.json",
"mldsa_65_verify_test.json",
"mldsa_87_sign_seed_test.json",
# "mldsa_87_sign_noseed_test.json",
"mldsa_87_verify_test.json",
]
def run_test(self, data: dict, group: dict, test: dict) -> None:
ctx = test.get("ctx")
if ctx is not None and ctx != "":
# Currently Botan doesn't support context (ctx), skip...
self.skipTest("ML-DSA ctx not supported")
if "msg" not in test or ("flags" in test and "Internal" in test["flags"]):
# Currently Botan does not provide the "Sign_internal" interface of ML-DSA,
# hence, signing without "msg" and only using "mu" isn't possible.
self.skipTest("ML-DSA's Sign_internal interface is not exposed")
mldsa_mode = _map_algorithm_to_mode(data.get("algorithm", ""))
group_type = group.get("type")
if group_type == "MlDsaSign":
self._run_sign_test(mldsa_mode, group, test)
elif group_type == "MlDsaVerify":
self._run_verify_test(mldsa_mode, group, test)
else:
self.fail(f"Unknown test group type: {group_type}")
def _run_sign_test(self, mldsa_mode: str, group: dict, test: dict) -> None:
"""Run a signing test (either seed or noseed variant)."""
# Load the private key
priv = None
try:
if "privateSeed" in group:
priv = botan.PrivateKey.load_ml_dsa(
mldsa_mode, _from_hex(group["privateSeed"])
)
# TODO: implement loading from expanded private key and enable the relevant
# input files with *_noseed_*.json
except botan.BotanException:
if test["result"] == "invalid":
return
raise
if priv is None:
self.fail("No private key available for signing test")
# Derive the public key and validate it against the expected public key
pub = priv.get_public_key()
if "publicKey" in group and group["publicKey"] is not None:
self.assertEqual(
pub.to_raw(),
_from_hex(group["publicKey"]),
"Deserialized public key does not match expected public key from test group",
)
# Perform signing
try:
signer = botan.PKSign(priv, "Deterministic")
signer.update(_from_hex(test["msg"]))
actual_sig = signer.finish(NullRNG())
except botan.BotanException:
if test["result"] == "invalid":
return
raise
# Validate the generated signature
self.assertEqual(
actual_sig,
_from_hex(test["sig"]),
"Generated signature does not match expected signature in test vector",
)
def _run_verify_test(self, mldsa_mode: str, group: dict, test: dict) -> None:
"""Run a verification test."""
# Load public key
try:
pub = botan.PublicKey.load_ml_dsa(mldsa_mode, _from_hex(group["publicKey"]))
except botan.BotanException:
if test["result"] == "invalid":
return
raise
# Perform verification
verifier = botan.PKVerify(pub, "")
verifier.update(_from_hex(test["msg"]))
valid = verifier.check_signature(_from_hex(test["sig"]))
# Validate the verification result
self.assertEqual(
valid,
test["result"] == "valid",
"Signature should be valid"
if test["result"] == "valid"
else "Signature should be invalid",
)