ML-DSA: Support (re-)generating MlDsaPrivate from seed

As specified in FIPS 204, implementations can store the seed from
which the key can be deterministically generated.
pull/89/head
Martijn de Milliano 2026-03-24 21:22:39 +01:00
parent 5b13e52d57
commit 1c034b20d8
3 changed files with 60 additions and 0 deletions

View File

@ -1026,6 +1026,7 @@ def build_ffi(local_wolfssl, features):
int wc_dilithium_set_level(dilithium_key* key, byte level);
void wc_dilithium_free(dilithium_key* key);
int wc_dilithium_make_key(dilithium_key* key, WC_RNG* rng);
int wc_dilithium_make_key_from_seed(dilithium_key* key, const byte* seed);
int wc_dilithium_export_private(dilithium_key* key, byte* out, word32* outLen);
int wc_dilithium_import_private(const byte* priv, word32 privSz, dilithium_key* key);
int wc_dilithium_export_public(dilithium_key* key, byte* out, word32* outLen);

View File

@ -28,6 +28,8 @@ if _lib.ML_DSA_ENABLED:
from wolfcrypt.ciphers import MlDsaPrivate, MlDsaPublic, MlDsaType
from wolfcrypt.random import Random
ML_DSA_SEED_LENGTH = 32
@pytest.fixture
def rng():
return Random()
@ -134,3 +136,35 @@ if _lib.ML_DSA_ENABLED:
# Verify with wrong message
wrong_message = b"This is a wrong message for ML-DSA signature"
assert not mldsa_pub.verify(signature, wrong_message)
def test_generate_from_seed(mldsa_type, rng):
private_key_seed = rng.bytes(ML_DSA_SEED_LENGTH)
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, private_key_seed)
pub_key = mldsa_priv.encode_pub_key()
# Import public key
mldsa_pub = MlDsaPublic(mldsa_type)
mldsa_pub.decode_key(pub_key)
# Sign a message
message = b"This is a test message for ML-DSA signature"
signature = mldsa_priv.sign(message, rng)
assert len(signature) == mldsa_priv.sig_size
# Verify the signature using public key
assert mldsa_pub.verify(signature, message)
# re-generate from the same seed:
mldsa_priv_regenerated = MlDsaPrivate.make_key_from_seed(mldsa_type, private_key_seed)
assert mldsa_priv_regenerated.encode_priv_key() == mldsa_priv.encode_priv_key()
assert mldsa_priv_regenerated.encode_pub_key() == mldsa_priv.encode_pub_key()
# test that the seed is checked:
with pytest.raises(AssertionError):
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, private_key_seed[:-1])
with pytest.raises(AssertionError):
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, private_key_seed + bytes(1))
with pytest.raises(AssertionError):
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, 'a' * 32)

View File

@ -2152,6 +2152,9 @@ if _lib.ML_DSA_ENABLED:
return res[0] == 1
class MlDsaPrivate(_MlDsaBase):
_SEED_LENGTH = 32
"""The length of a private key generation seed."""
@classmethod
def make_key(cls, mldsa_type, rng=Random()):
"""
@ -2172,6 +2175,28 @@ if _lib.ML_DSA_ENABLED:
return mldsa_priv
@classmethod
def make_key_from_seed(cls, mldsa_type, seed):
"""
Deterministically generate the key from a seed.
:param mldsa_type: ML-DSA type
:type mldsa_type: MlDsaType
:param seed: the (32 byte) seed from which to deterministically create the key
:type seed: bytes
"""
mldsa_priv = cls(mldsa_type)
assert isinstance(seed, bytes) and len(seed) == MlDsaPrivate._SEED_LENGTH, \
f"Seed for generating ML-DSA key must be {MlDsaPrivate._SEED_LENGTH} bytes"
ret = _lib.wc_dilithium_make_key_from_seed(mldsa_priv.native_object,
_ffi.from_buffer(seed))
if ret < 0: # pragma: no cover
raise WolfCryptError("wc_dilithium_make_key_from_seed() error (%d)" % ret)
return mldsa_priv
@property
def pub_key_size(self):
"""