Merge branch 'master' into ml-dsa-generate-from-seed

pull/89/head
Daniele Lacamera 2026-04-15 06:56:56 +02:00 committed by GitHub
commit 90f21e1d13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 150 additions and 59 deletions

View File

@ -26,7 +26,6 @@ from contextlib import contextmanager
from distutils.util import get_platform
from cffi import FFI
import shutil
import glob
from wolfcrypt._version import __wolfssl_version__ as version
def local_path(path):
@ -332,7 +331,7 @@ def get_features(local_wolfssl, features):
for d in include_dirs:
if not os.path.exists(d):
e = "Invalid wolfSSL include dir: .".format(d)
e = f"Invalid wolfSSL include dir: {d}"
raise FileNotFoundError(e)
options = os.path.join(d, "wolfssl", "options.h")
@ -543,6 +542,8 @@ def build_ffi(local_wolfssl, features):
typedef struct { ...; } OS_Seed;
int wc_InitRng(WC_RNG*);
int wc_InitRngNonce(WC_RNG*, byte*, word32);
int wc_InitRngNonce_ex(WC_RNG*, byte*, word32, void*, int);
int wc_RNG_GenerateBlock(WC_RNG*, byte*, word32);
int wc_RNG_GenerateByte(WC_RNG*, byte*);
int wc_FreeRng(WC_RNG*);
@ -1032,6 +1033,8 @@ def build_ffi(local_wolfssl, features):
int wc_dilithium_export_public(dilithium_key* key, byte* out, word32* outLen);
int wc_dilithium_import_public(const byte* in, word32 inLen, dilithium_key* key);
int wc_dilithium_sign_msg(const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, WC_RNG* rng);
int wc_dilithium_sign_msg_with_seed(const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, const byte* seed);
int wc_dilithium_sign_ctx_msg_with_seed(const byte* ctx, byte ctxLen, const byte* msg, word32 msgLen, byte* sig, word32* sigLen, dilithium_key* key, const byte* seed);
int wc_dilithium_verify_msg(const byte* sig, word32 sigLen, const byte* msg, word32 msgLen, int* res, dilithium_key* key);
typedef dilithium_key MlDsaKey;
int wc_MlDsaKey_GetPrivLen(MlDsaKey* key, int* len);

View File

@ -22,12 +22,12 @@
# pylint: disable=wrong-import-position
import os
import re
import sys
from setuptools import setup, find_packages
os.chdir(os.path.dirname(sys.argv[0]) or ".")
import re
VERSIONFILE = "wolfcrypt/_version.py"
verstrline = open(VERSIONFILE, "rt").read()
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"

View File

@ -23,11 +23,10 @@
from wolfcrypt._ffi import lib as _lib
if _lib.AESGCM_STREAM_ENABLED:
from collections import namedtuple
import pytest
from wolfcrypt.utils import t2b
from wolfcrypt.exceptions import WolfCryptError
from binascii import hexlify as b2h, unhexlify as h2b
from binascii import hexlify as b2h
from wolfcrypt.ciphers import AesGcmStream
def test_encrypt():

View File

@ -27,7 +27,7 @@ if _lib.CHACHA20_POLY1305_ENABLED:
import pytest
from wolfcrypt.utils import t2b
from wolfcrypt.exceptions import WolfCryptError
from binascii import hexlify as b2h, unhexlify as h2b
from binascii import unhexlify as h2b
from wolfcrypt.ciphers import ChaCha20Poly1305
def test_encrypt_decrypt():

View File

@ -23,8 +23,8 @@
from collections import namedtuple
import random
import pytest
from wolfcrypt._ffi import ffi as _ffi
from wolfcrypt._ffi import lib as _lib
from wolfcrypt.ciphers import MODE_CTR, MODE_ECB, MODE_CBC, WolfCryptError
from wolfcrypt.utils import t2b, h2b
import os
@ -43,20 +43,16 @@ if _lib.CHACHA_ENABLED:
from wolfcrypt.ciphers import ChaCha
if _lib.RSA_ENABLED:
from wolfcrypt.ciphers import (RsaPrivate, RsaPublic, HASH_TYPE_SHA256, MGF1SHA256, HASH_TYPE_SHA, MGF1SHA1)
from wolfcrypt.ciphers import RsaPrivate, RsaPublic, HASH_TYPE_SHA256, HASH_TYPE_SHA
if _lib.ECC_ENABLED:
from wolfcrypt.ciphers import (EccPrivate, EccPublic)
from wolfcrypt.ciphers import EccPrivate, EccPublic
if _lib.ED25519_ENABLED:
from wolfcrypt.ciphers import (Ed25519Private, Ed25519Public)
from wolfcrypt.ciphers import Ed25519Private, Ed25519Public
if _lib.ED448_ENABLED:
from wolfcrypt.ciphers import (Ed448Private, Ed448Public)
from wolfcrypt.ciphers import (
MODE_CTR, MODE_ECB, MODE_CBC, WolfCryptError
)
from wolfcrypt.ciphers import Ed448Private, Ed448Public
@pytest.fixture

View File

@ -22,7 +22,6 @@
from collections import namedtuple
import pytest
from wolfcrypt._ffi import ffi as _ffi
from wolfcrypt._ffi import lib as _lib
from wolfcrypt.utils import t2b

View File

@ -24,8 +24,7 @@ from wolfcrypt._ffi import lib as _lib
if _lib.ML_DSA_ENABLED:
import pytest
from wolfcrypt.ciphers import MlDsaPrivate, MlDsaPublic, MlDsaType, ML_DSA_KEYGEN_SEED_LENGTH
from wolfcrypt.ciphers import MlDsaPrivate, MlDsaPublic, MlDsaType, ML_DSA_KEYGEN_SEED_LENGTH, ML_DSA_SIGNATURE_SEED_LENGTH
from wolfcrypt.random import Random
@pytest.fixture
@ -135,9 +134,9 @@ if _lib.ML_DSA_ENABLED:
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_KEYGEN_SEED_LENGTH)
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, private_key_seed)
def test_sign_with_seed(mldsa_type, rng):
signature_seed = rng.bytes(ML_DSA_SIGNATURE_SEED_LENGTH)
mldsa_priv = MlDsaPrivate.make_key(mldsa_type, rng)
pub_key = mldsa_priv.encode_pub_key()
# Import public key
@ -146,22 +145,38 @@ if _lib.ML_DSA_ENABLED:
# Sign a message
message = b"This is a test message for ML-DSA signature"
signature = mldsa_priv.sign(message, rng)
signature = mldsa_priv.sign_with_seed(message, signature_seed)
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()
signature_from_same_seed = mldsa_priv.sign_with_seed(message, signature_seed)
assert signature == signature_from_same_seed
# test that the seed length is checked:
# test that the seed size is checked:
with pytest.raises(ValueError):
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, bytes(ML_DSA_KEYGEN_SEED_LENGTH - 1))
with pytest.raises(ValueError):
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, bytes(ML_DSA_KEYGEN_SEED_LENGTH + 1))
# test that the seed type is checked (should be bytes-like, not a string)
_ = mldsa_priv.sign_with_seed(message, signature_seed[:-1])
# test that the seed type is checked (should be bytes-like, not string)
with pytest.raises(TypeError):
mldsa_priv = MlDsaPrivate.make_key_from_seed(mldsa_type, 'a' * ML_DSA_KEYGEN_SEED_LENGTH)
_ = mldsa_priv.sign_with_seed(message, "")
def test_sign_with_seed_and_context(mldsa_type, rng):
signature_seed = rng.bytes(ML_DSA_SIGNATURE_SEED_LENGTH)
mldsa_priv = MlDsaPrivate.make_key(mldsa_type, rng)
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"
context = b"Some context for the signature"
signature = mldsa_priv.sign_with_seed(message, signature_seed, ctx=context)
assert len(signature) == mldsa_priv.sig_size
# test that the context length is checked (more than 255 bytes is invalid):
with pytest.raises(ValueError):
_ = mldsa_priv.sign_with_seed(message, signature_seed[:-1], ctx=bytes(1000))

View File

@ -37,3 +37,14 @@ def test_bytes(rng):
assert len(rng.bytes(1)) == 1
assert len(rng.bytes(8)) == 8
assert len(rng.bytes(128)) == 128
@pytest.fixture
def rng_nonce():
return Random(b"abcdefghijklmnopqrstuv")
def test_nonce_byte(rng_nonce):
assert len(rng_nonce.byte()) == 1
@pytest.mark.parametrize("length", (1, 8, 128))
def test_nonce_bytes(rng_nonce, length):
assert len(rng_nonce.bytes(length)) == length

View File

@ -46,6 +46,7 @@ top_level_py = os.path.basename(sys.argv[0])
if top_level_py not in ["setup.py", "build_ffi.py"]:
from wolfcrypt._ffi import ffi as _ffi
from wolfcrypt._ffi import lib as _lib
from wolfcrypt.exceptions import WolfCryptError
if hasattr(_lib, 'WC_RNG_SEED_CB_ENABLED'):
if _lib.WC_RNG_SEED_CB_ENABLED:
@ -53,8 +54,7 @@ if top_level_py not in ["setup.py", "build_ffi.py"]:
if ret < 0:
raise WolfCryptError("wc_SetSeed_Cb failed (%d)" % ret)
if _lib.FIPS_ENABLED and _lib.FIPS_VERSION >= 5:
ret = _lib.wolfCrypt_SetPrivateKeyReadEnable_fips(1,
_lib.WC_KEYTYPE_ALL);
ret = _lib.wolfCrypt_SetPrivateKeyReadEnable_fips(1, _lib.WC_KEYTYPE_ALL)
if ret < 0:
raise WolfCryptError("wolfCrypt_SetPrivateKeyReadEnable_fips failed"
" (%d)" % ret)

View File

@ -213,10 +213,9 @@ class _Cipher(object):
string = t2b(string)
if not string:
raise ValueError(
"empty string not allowed")
raise ValueError("empty string not allowed")
if len(string) % self.block_size and not self.mode == MODE_CTR and not "ChaCha" in self._native_type:
if len(string) % self.block_size and self.mode != MODE_CTR and "ChaCha" not in self._native_type:
raise ValueError(
"string must be a multiple of %d in length" % self.block_size)
@ -498,7 +497,7 @@ if _lib.CHACHA_ENABLED:
self._dec = None
self._key = None
if len(key) > 0:
if not size in self._key_sizes:
if size not in self._key_sizes:
raise ValueError("Invalid key size %d" % size)
self._key = t2b(key)
self.key_size = size
@ -506,7 +505,7 @@ if _lib.CHACHA_ENABLED:
self._IV_counter = 0
def _set_key(self, direction):
if self._key == None:
if self._key is None:
return -1
if self._enc:
ret = _lib.wc_Chacha_SetKey(self._enc, self._key, len(self._key))
@ -692,7 +691,7 @@ if _lib.RSA_ENABLED:
class RsaPublic(_Rsa):
def __init__(self, key=None, hash_type=None):
if key != None:
if key is not None:
key = t2b(key)
self._hash_type = hash_type
@ -830,8 +829,6 @@ if _lib.RSA_ENABLED:
Generates a new key pair of desired length **size**.
"""
rsa = cls(hash_type=hash_type)
if rsa == None: # pragma: no cover
raise WolfCryptError("Invalid key error (%d)" % ret)
ret = _lib.wc_MakeRsaKey(rsa.native_object, size, 65537,
rng.native_object)
@ -852,7 +849,7 @@ if _lib.RSA_ENABLED:
idx = _ffi.new("word32*")
idx[0] = 0
if key != None:
if key is not None:
key = t2b(key)
ret = _lib.wc_RsaPrivateKeyDecode(key, idx,
self.native_object, len(key))
@ -1096,7 +1093,7 @@ if _lib.ECC_ENABLED:
qy_size[0] = self.size
ret = _lib.wc_ecc_export_public_raw(self.native_object, Qx,
qx_size, Qy, qy_size);
qx_size, Qy, qy_size)
if ret != 0: # pragma: no cover
raise WolfCryptError("Key encode error (%d)" % ret)
@ -1265,7 +1262,7 @@ if _lib.ECC_ENABLED:
d_size[0] = self.size
ret = _lib.wc_ecc_export_private_raw(self.native_object, Qx,
qx_size, Qy, qy_size, d, d_size);
qx_size, Qy, qy_size, d, d_size)
if ret != 0: # pragma: no cover
raise WolfCryptError("Key encode error (%d)" % ret)
@ -1322,8 +1319,8 @@ if _lib.ECC_ENABLED:
Returns the signature in its two raw components r, s
"""
plaintext = t2b(plaintext)
R = _ffi.new("mp_int[1]");
S = _ffi.new("mp_int[1]");
R = _ffi.new("mp_int[1]")
S = _ffi.new("mp_int[1]")
R_bin = _ffi.new("unsigned char[%d]" % self.size )
S_bin = _ffi.new("unsigned char[%d]" % self.size )
@ -1478,12 +1475,12 @@ if _lib.ED25519_ENABLED:
idx[0] = 0
if pub:
ret = _lib.wc_ed25519_import_private_key(key, len(key), pub,
len(pub), self.native_object);
len(pub), self.native_object)
if ret < 0:
raise WolfCryptError("Key decode error (%d)" % ret)
else:
ret = _lib.wc_ed25519_import_private_only(key, len(key),
self.native_object);
self.native_object)
if ret < 0:
raise WolfCryptError("Key decode error (%d)" % ret)
pubkey = _ffi.new("byte[%d]" % (self.size * 4))
@ -1492,7 +1489,7 @@ if _lib.ED25519_ENABLED:
if ret < 0:
raise WolfCryptError("Public key generate error (%d)" % ret)
ret = _lib.wc_ed25519_import_public(pubkey, self.size,
self.native_object);
self.native_object)
if self.size <= 0: # pragma: no cover
raise WolfCryptError("Key decode error (%d)" % self.size)
@ -1622,7 +1619,7 @@ if _lib.ED448_ENABLED:
status = _ffi.new("int[1]")
ctx_buf = _ffi.NULL
ctx_buf_len = 0
if ctx != None:
if ctx is not None:
ctx_buf = t2b(ctx)
ctx_buf_len = len(ctx_buf)
@ -1674,12 +1671,12 @@ if _lib.ED448_ENABLED:
idx[0] = 0
if pub:
ret = _lib.wc_ed448_import_private_key(key, len(key), pub,
len(pub), self.native_object);
len(pub), self.native_object)
if ret < 0:
raise WolfCryptError("Key decode error (%d)" % ret)
else:
ret = _lib.wc_ed448_import_private_only(key, len(key),
self.native_object);
self.native_object)
if ret < 0:
raise WolfCryptError("Key decode error (%d)" % ret)
pubkey = _ffi.new("byte[%d]" % (self.size * 4))
@ -1688,7 +1685,7 @@ if _lib.ED448_ENABLED:
if ret < 0:
raise WolfCryptError("Public key generate error (%d)" % ret)
ret = _lib.wc_ed448_import_public(pubkey, self.size,
self.native_object);
self.native_object)
if self.size <= 0: # pragma: no cover
raise WolfCryptError("Key decode error (%d)" % self.size)
@ -1732,7 +1729,7 @@ if _lib.ED448_ENABLED:
signature_size[0] = self.max_signature_size
ctx_buf = _ffi.NULL
ctx_buf_len = 0
if (ctx != None):
if ctx is not None:
ctx_buf = t2b(ctx)
ctx_buf_len = len(ctx_buf)
@ -2031,9 +2028,9 @@ if _lib.ML_KEM_ENABLED:
if _lib.ML_DSA_ENABLED:
ML_DSA_KEYGEN_SEED_LENGTH = 32
"""The length of a private key generation seed."""
ML_DSA_SIGNATURE_SEED_LENGTH = 32
"""The length of a signature generation seed."""
class MlDsaType(IntEnum):
"""
`MlDsaType` specifies supported ML-DSA types.
@ -2314,6 +2311,73 @@ if _lib.ML_DSA_ENABLED:
return _ffi.buffer(signature, out_size[0])[:]
def sign_with_seed(self, message, seed, ctx=None):
"""
:param message: message to be signed
:type message: bytes or str
:param seed: 32-byte seed for deterministic signature generation.
:type seed: bytes
:param ctx: context (optional, maximum 255 bytes)
:type ctx: None for no context, str or bytes otherwise
:return: signature
:rtype: bytes
"""
msg_bytestype = t2b(message)
in_size = self.sig_size
signature = _ffi.new(f"byte[{in_size}]")
out_size = _ffi.new("word32 *")
out_size[0] = in_size
try:
seed_view = memoryview(seed)
except TypeError as exception:
raise TypeError(
"seed must support the buffer protocol, such as `bytes` or `bytearray`"
) from exception
if len(seed_view) != ML_DSA_SIGNATURE_SEED_LENGTH:
raise ValueError(
f"Seed for generating a signature must be {ML_DSA_SIGNATURE_SEED_LENGTH}"
"bytes."
)
if ctx is not None:
ctx_bytestype = t2b(ctx)
if len(ctx_bytestype) > 255:
raise ValueError(
f"context length {len(ctx_bytestype)} too large: must be 255 or less"
)
ret = _lib.wc_dilithium_sign_ctx_msg_with_seed(
_ffi.from_buffer(ctx_bytestype),
len(ctx_bytestype), # length must be < 256 bytes
_ffi.from_buffer(msg_bytestype),
len(msg_bytestype),
signature,
out_size,
self.native_object,
_ffi.from_buffer(seed_view),
)
if ret < 0: # pragma: no cover
raise WolfCryptError("wc_dilithium_sign_ctx_msg_with_seed() error (%d)" % ret)
else:
ret = _lib.wc_dilithium_sign_msg_with_seed(
_ffi.from_buffer(msg_bytestype),
len(msg_bytestype),
signature,
out_size,
self.native_object,
_ffi.from_buffer(seed_view),
)
if ret < 0: # pragma: no cover
raise WolfCryptError("wc_dilithium_sign_msg_with_seed() error (%d)" % ret)
if in_size != out_size[0]:
raise WolfCryptError(
"in_size=%d and out_size=%d don't match" % (in_size, out_size[0])
)
return _ffi.buffer(signature, out_size[0])[:]
class MlDsaPublic(_MlDsaBase):
@property
def key_size(self):

View File

@ -31,10 +31,14 @@ class Random(object):
A Cryptographically Secure Pseudo Random Number Generator - CSPRNG
"""
def __init__(self):
def __init__(self, nonce=_ffi.NULL, device_id=_lib.INVALID_DEVID):
self.native_object = _ffi.new("WC_RNG *")
ret = _lib.wc_InitRng(self.native_object)
if nonce == _ffi.NULL:
nonce_size = 0
else:
nonce_size = len(nonce)
ret = _lib.wc_InitRngNonce_ex(self.native_object, nonce, nonce_size, _ffi.NULL, device_id)
if ret < 0: # pragma: no cover
self.native_object = None
raise WolfCryptError("RNG init error (%d)" % ret)