diff --git a/scripts/build_ffi.py b/scripts/build_ffi.py index d0ff8b2..7445ca8 100644 --- a/scripts/build_ffi.py +++ b/scripts/build_ffi.py @@ -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); diff --git a/setup.py b/setup.py index 7c16e74..ed2836e 100755 --- a/setup.py +++ b/setup.py @@ -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__ = ['\"]([^'\"]*)['\"]" diff --git a/tests/test_aesgcmstream.py b/tests/test_aesgcmstream.py index 12c8c04..61ad285 100644 --- a/tests/test_aesgcmstream.py +++ b/tests/test_aesgcmstream.py @@ -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(): diff --git a/tests/test_chacha20poly1305.py b/tests/test_chacha20poly1305.py index c67840e..07616ea 100644 --- a/tests/test_chacha20poly1305.py +++ b/tests/test_chacha20poly1305.py @@ -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(): diff --git a/tests/test_ciphers.py b/tests/test_ciphers.py index 79092e2..2cbd483 100644 --- a/tests/test_ciphers.py +++ b/tests/test_ciphers.py @@ -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 diff --git a/tests/test_hashes.py b/tests/test_hashes.py index 3f5c2de..09eeb26 100644 --- a/tests/test_hashes.py +++ b/tests/test_hashes.py @@ -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 diff --git a/tests/test_mldsa.py b/tests/test_mldsa.py index de8589f..dcb3ce0 100644 --- a/tests/test_mldsa.py +++ b/tests/test_mldsa.py @@ -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)) diff --git a/tests/test_random.py b/tests/test_random.py index c95847a..bf59f4e 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -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 diff --git a/wolfcrypt/__init__.py b/wolfcrypt/__init__.py index 6879589..d7e8721 100644 --- a/wolfcrypt/__init__.py +++ b/wolfcrypt/__init__.py @@ -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) diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index 6191236..3d0bb0b 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -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): diff --git a/wolfcrypt/random.py b/wolfcrypt/random.py index c576807..9c9f6b6 100644 --- a/wolfcrypt/random.py +++ b/wolfcrypt/random.py @@ -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)