Merge pull request #118 from LinuxJedi/f-fixes

Fenrir fixes
pull/119/head
David Garske 2026-05-11 10:27:11 -07:00 committed by GitHub
commit c6d23d30f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 255 additions and 19 deletions

View File

@ -38,3 +38,55 @@ jobs:
- name: Test with tox
run: |
tox
build-no-pqc:
# Regression coverage for issue #2659: INVALID_DEVID is only declared
# in the CFFI cdef when ML_KEM or ML_DSA is enabled, so a wolfSSL build
# without those features must still import wolfcrypt.random successfully.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install -y autoconf automake libtool
- name: Build wolfSSL without ML-KEM/ML-DSA
run: |
cd lib/wolfssl
./autogen.sh
./configure --enable-cryptonly --disable-shared \
--disable-kyber --disable-dilithium \
--enable-aes --enable-aesgcm --enable-aessiv \
--enable-aesctr --enable-aesgcm-stream \
--enable-des3 --enable-chacha --enable-poly1305 \
--enable-sha --enable-sha384 --enable-sha512 --enable-sha3 \
--enable-hkdf --enable-rsa --enable-rsapss --enable-ecc \
--enable-ed25519 --enable-ed448 --enable-curve25519 \
--enable-keygen --enable-pwdbased --enable-pkcs7 \
--disable-md5 --disable-sha224 --disable-dh \
--disable-oldtls --disable-oldnames --disable-extended-master \
CFLAGS=-fPIC \
--prefix=$GITHUB_WORKSPACE/wolfssl-install
make
make install
- name: Install wolfcrypt-py against the local wolfSSL
env:
USE_LOCAL_WOLFSSL: ${{ github.workspace }}/wolfssl-install
run: |
python -m pip install --upgrade pip
pip install -r requirements/test.txt
pip install -e .
- name: Import smoke (regression for INVALID_DEVID)
run: python -c "from wolfcrypt.random import Random; Random()"
- name: Run tests
env:
USE_LOCAL_WOLFSSL: ${{ github.workspace }}/wolfssl-install
run: pytest tests/

View File

@ -1051,7 +1051,7 @@ def build_ffi(local_wolfssl, features):
cdef += """
int wc_RsaPSS_Sign(const byte* in, word32 inLen, byte* out, word32 outLen,
enum wc_HashType hash, int mgf, RsaKey* key, WC_RNG* rng);
int wc_RsaPSS_Verify(byte* in, word32 inLen, byte* out, word32 outLen,
int wc_RsaPSS_Verify(const byte* in, word32 inLen, byte* out, word32 outLen,
enum wc_HashType hash, int mgf, RsaKey* key);
int wc_RsaPSS_CheckPadding(const byte* in, word32 inSz, byte* sig,
word32 sigSz, enum wc_HashType hashType);
@ -1092,10 +1092,11 @@ def build_ffi(local_wolfssl, features):
int wc_ecc_import_x963(const byte* in, word32 inLen, ecc_key* key);
int wc_ecc_export_private_raw(ecc_key* key, byte* qx, word32* qxLen,
byte* qy, word32* qyLen, byte* d, word32* dLen);
int wc_ecc_import_unsigned(ecc_key* key, byte* qx, byte* qy,
byte* d, int curve_id);
int wc_ecc_import_unsigned(ecc_key* key, const byte* qx, const byte* qy,
const byte* d, int curve_id);
int wc_ecc_export_public_raw(ecc_key* key, byte* qx, word32* qxLen,
byte* qy, word32* qyLen);
int wc_ecc_get_curve_size_from_id(int curve_id);
int wc_ecc_shared_secret(ecc_key* private_key, ecc_key* public_key,
@ -1172,11 +1173,11 @@ def build_ffi(local_wolfssl, features):
int wc_ed448_size(ed448_key* key);
int wc_ed448_sig_size(ed448_key* key);
int wc_ed448_sign_msg(const byte* in, word32 inlen, byte* out,
word32 *outlen, ed448_key* key, byte* ctx,
word32 ctx_len);
word32 *outlen, ed448_key* key, const byte* ctx,
byte ctx_len);
int wc_ed448_verify_msg(const byte* sig, word32 siglen, const byte* msg,
word32 msglen, int* stat, ed448_key* key, byte *ctx,
word32 ctx_len);
word32 msglen, int* stat, ed448_key* key, const byte *ctx,
byte ctx_len);
int wc_Ed448PrivateKeyDecode(const byte*, word32*, ed448_key*, word32);
int wc_Ed448KeyToDer(ed448_key*, byte* output, word32 inLen);

View File

@ -614,6 +614,37 @@ if _lib.ECC_ENABLED:
assert qy[0:32] == vectors[EccPublic].raw_key[32:64]
def test_ecc_decode_key_raw_rejects_wrong_length(vectors):
"""
wc_ecc_import_unsigned reads exactly curve_size bytes from each
of qx/qy/d with no length information from the caller. The
Python wrappers must validate the lengths up-front so a short
buffer cannot cause an out-of-bounds read in the C library.
"""
raw_priv = EccPrivate()
raw_pub = EccPublic()
key = vectors[EccPrivate].raw_key
qx_good, qy_good, d_good = key[0:32], key[32:64], key[64:96]
# Short qx
with pytest.raises(ValueError, match="must each be 32 bytes"):
raw_pub.decode_key_raw(qx_good[:-1], qy_good)
with pytest.raises(ValueError, match="must each be 32 bytes"):
raw_priv.decode_key_raw(qx_good[:-1], qy_good, d_good)
# Long qy
with pytest.raises(ValueError, match="must each be 32 bytes"):
raw_pub.decode_key_raw(qx_good, qy_good + b"\x00")
with pytest.raises(ValueError, match="must each be 32 bytes"):
raw_priv.decode_key_raw(qx_good, qy_good + b"\x00", d_good)
# Short d
with pytest.raises(ValueError, match="must each be 32 bytes"):
raw_priv.decode_key_raw(qx_good, qy_good, d_good[:-1])
# Unknown curve id
with pytest.raises(ValueError, match="Unknown ECC curve_id"):
raw_pub.decode_key_raw(qx_good, qy_good, curve_id=-99999)
# Happy path still works after validation
raw_pub.decode_key_raw(qx_good, qy_good)
raw_priv.decode_key_raw(qx_good, qy_good, d_good)
@ -938,6 +969,35 @@ def test_aessiv_decrypt_kat_openssl():
assert plaintext == TEST_VECTOR_PLAINTEXT_OPENSSL
@pytest.mark.skipif(not _lib.AES_SIV_ENABLED, reason="AES-SIV not enabled")
@pytest.mark.parametrize("wrap", [bytes, bytearray, memoryview],
ids=["bytes", "bytearray", "memoryview"])
def test_aessiv_associated_data_accepts_buffer_types(wrap):
"""
Single-block associated_data passed as bytes, bytearray, or memoryview
must all produce the same SIV/ciphertext as the OpenSSL KAT. A previous
bug treated bytearray/memoryview as a sequence of int blocks, producing
a different (incorrect) tag without raising.
"""
aessiv = AesSiv(TEST_VECTOR_KEY_OPENSSL)
associated_data = wrap(TEST_VECTOR_ASSOCIATED_DATA_OPENSSL)
siv, ciphertext = aessiv.encrypt(
associated_data,
TEST_VECTOR_NONCE_OPENSSL,
TEST_VECTOR_PLAINTEXT_OPENSSL
)
assert siv == TEST_VECTOR_SIV_OPENSSL
assert ciphertext == TEST_VECTOR_CIPHERTEXT_OPENSSL
plaintext = aessiv.decrypt(
wrap(TEST_VECTOR_ASSOCIATED_DATA_OPENSSL),
TEST_VECTOR_NONCE_OPENSSL,
TEST_VECTOR_SIV_OPENSSL,
TEST_VECTOR_CIPHERTEXT_OPENSSL
)
assert plaintext == TEST_VECTOR_PLAINTEXT_OPENSSL
if _lib.DES3_ENABLED:
def test_des3_rejects_mode_ctr():
key = b"\x01\x23\x45\x67\x89\xab\xcd\xef" * 3
@ -969,6 +1029,83 @@ if _lib.CHACHA_ENABLED:
with pytest.raises(ValueError, match="key must be"):
ChaCha(b"\x00" * 20)
def test_chacha_decrypt_does_not_reset_encrypt_stream():
"""
Interleaving decrypt() between two encrypt() calls on the same
ChaCha instance must not reset the encryption stream counter, and
symmetrically interleaving encrypt() between two decrypt() calls
must not reset the decryption stream counter. A previous bug in
_set_key re-keyed both contexts whenever either was allocated, so
the first call to the other direction (which lazily allocates the
opposite context) silently rewound the existing stream to
counter 0, producing the wrong ciphertext/plaintext on subsequent
calls.
"""
key = b"\x00" * 32
nonce = b"\x00" * 12
block1 = b"A" * 64
block2 = b"B" * 64
# --- encrypt -> decrypt -> encrypt: the lazy _dec must not wipe _enc.
baseline = ChaCha(key)
baseline.set_iv(nonce)
expected_ct1 = baseline.encrypt(block1)
expected_ct2 = baseline.encrypt(block2)
assert expected_ct1 != expected_ct2
chacha = ChaCha(key)
chacha.set_iv(nonce)
ct1 = chacha.encrypt(block1)
assert ct1 == expected_ct1
chacha.decrypt(b"\x00" * 16)
ct2 = chacha.encrypt(block2)
assert ct2 == expected_ct2
# --- decrypt -> encrypt -> decrypt: the lazy _enc must not wipe _dec.
# Pre-compute the two ciphertexts that would decrypt back to
# block1, block2 in stream order.
producer = ChaCha(key)
producer.set_iv(nonce)
ct_a = producer.encrypt(block1)
ct_b = producer.encrypt(block2)
chacha = ChaCha(key)
chacha.set_iv(nonce)
pt1 = chacha.decrypt(ct_a)
assert pt1 == block1
# encrypt() now lazily allocates _enc; it must not reset _dec.
chacha.encrypt(b"\x00" * 16)
pt2 = chacha.decrypt(ct_b)
assert pt2 == block2
def test_chacha_set_iv_resets_both_directions():
"""
set_iv() is documented to reset the stream, and the existing
implementation relies on _set_key re-keying both contexts when
the IV changes. Lock that behavior in so a fix to the
interleave bug does not regress set_iv semantics.
"""
key = b"\x00" * 32
nonce_a = b"\x00" * 12
nonce_b = b"\x01" + b"\x00" * 11
plaintext = b"Z" * 32
chacha = ChaCha(key)
chacha.set_iv(nonce_a)
ct_a1 = chacha.encrypt(plaintext)
# Allocate the decryption context too.
chacha.decrypt(b"\x00" * 16)
# Changing IV must reset both contexts: subsequent encrypt/decrypt
# under nonce_b must match a freshly-keyed instance under nonce_b.
chacha.set_iv(nonce_b)
ct_b = chacha.encrypt(plaintext)
pt_back = chacha.decrypt(ct_b)
assert pt_back == plaintext
fresh = ChaCha(key)
fresh.set_iv(nonce_b)
assert fresh.encrypt(plaintext) == ct_b
if _lib.RSA_ENABLED:
def test_encrypt_oaep_requires_hash_type(vectors):

View File

@ -299,8 +299,9 @@ if _lib.AES_SIV_ENABLED:
Encrypt plaintext data using the nonce provided. The associated
data is not encrypted but is included in the authentication tag.
Associated data may be provided as single str or bytes, or as a
list of str or bytes in case of multiple blocks.
Associated data may be provided as a single str, bytes,
bytearray, or memoryview, or as a list of any of those in case
of multiple blocks.
Returns a tuple of the IV and ciphertext.
"""
@ -325,8 +326,9 @@ if _lib.AES_SIV_ENABLED:
Decrypt the ciphertext using the nonce and SIV provided.
The integrity of the associated data is checked.
Associated data may be provided as single str or bytes, or as a
list of str or bytes in case of multiple blocks.
Associated data may be provided as a single str, bytes,
bytearray, or memoryview, or as a list of any of those in case
of multiple blocks.
Returns the decrypted plaintext.
"""
@ -354,8 +356,9 @@ if _lib.AES_SIV_ENABLED:
"""
Prepare associated data for sending to C library.
Associated data may be provided as single str or bytes, or as a
list of str or bytes in case of multiple blocks.
Associated data may be provided as a single str, bytes,
bytearray, or memoryview, or as a list of any of those in case
of multiple blocks.
The result is a tuple of the list of cffi cdata pointers to
AesSivAssoc structures, as well as the converted associated
@ -363,7 +366,7 @@ if _lib.AES_SIV_ENABLED:
C function has been called, in order to make sure that the memory
is not freed by the FFI garbage collector before the data is read.
"""
if isinstance(associated_data, str) or isinstance(associated_data, bytes):
if isinstance(associated_data, (str, bytes, bytearray, memoryview)):
# A single block is provided.
# Make sure we have bytes.
associated_data = t2b(associated_data)
@ -527,16 +530,27 @@ if _lib.CHACHA_ENABLED:
self._IV_nonce = b""
self._IV_counter = 0
# Sentinel for "rekey both contexts" used by set_iv. Must not
# collide with _ENCRYPTION (0) or _DECRYPTION (1).
_REKEY_BOTH = -1
def _set_key(self, direction):
if self._key is None:
return -1
if self._enc:
# _REKEY_BOTH re-keys whichever contexts are already allocated,
# since changing the IV must reset both encrypt and decrypt
# streams. _ENCRYPTION / _DECRYPTION only touch the matching
# context so that lazy allocation from encrypt()/decrypt() does
# not wipe the other direction's stream state.
do_enc = self._enc and direction in (self._REKEY_BOTH, _ENCRYPTION)
do_dec = self._dec and direction in (self._REKEY_BOTH, _DECRYPTION)
if do_enc:
ret = _lib.wc_Chacha_SetKey(self._enc, self._key, len(self._key))
if ret == 0:
ret = _lib.wc_Chacha_SetIV(self._enc, self._IV_nonce, self._IV_counter)
if ret != 0:
return ret
if self._dec:
if do_dec:
ret = _lib.wc_Chacha_SetKey(self._dec, self._key, len(self._key))
if ret == 0:
ret = _lib.wc_Chacha_SetIV(self._dec, self._IV_nonce, self._IV_counter)
@ -560,7 +574,7 @@ if _lib.CHACHA_ENABLED:
raise ValueError("nonce must be %d bytes, got %d" %
(self._NONCE_SIZE, len(self._IV_nonce)))
self._IV_counter = counter
ret = self._set_key(0)
ret = self._set_key(self._REKEY_BOTH)
if ret < 0:
raise WolfCryptApiError("ChaCha set_iv error", ret)
@ -896,7 +910,12 @@ if _lib.RSA_ENABLED:
self.native_object, len(key))
if ret < 0:
idx[0] = 0
ret = _lib.wc_GetPkcs8TraditionalOffset(key, idx, len(key))
# wc_GetPkcs8TraditionalOffset takes byte* (non-const) per
# the wolfSSL public header, so route it through a CFFI-
# owned buffer rather than handing it a writable pointer
# into the Python bytes object.
key_buf = _ffi.new("byte[]", key)
ret = _lib.wc_GetPkcs8TraditionalOffset(key_buf, idx, len(key))
if ret < 0:
raise WolfCryptApiError("Invalid key error", ret)
@ -1099,6 +1118,15 @@ if _lib.ECC_ENABLED:
"""
Decodes an ECC public key from its raw elements: (Qx,Qy)
"""
qx = t2b(qx)
qy = t2b(qy)
curve_size = _lib.wc_ecc_get_curve_size_from_id(curve_id)
if curve_size <= 0:
raise ValueError("Unknown ECC curve_id %d" % curve_id)
if len(qx) != curve_size or len(qy) != curve_size:
raise ValueError(
"qx and qy must each be %d bytes for curve_id %d, got "
"qx=%d qy=%d" % (curve_size, curve_id, len(qx), len(qy)))
ret = _lib.wc_ecc_import_unsigned(self.native_object, qx, qy,
_ffi.NULL, curve_id)
if ret != 0:
@ -1277,6 +1305,18 @@ if _lib.ECC_ENABLED:
Decodes an ECC private key from its raw elements: public (Qx,Qy)
and private(d)
"""
qx = t2b(qx)
qy = t2b(qy)
d = t2b(d)
curve_size = _lib.wc_ecc_get_curve_size_from_id(curve_id)
if curve_size <= 0:
raise ValueError("Unknown ECC curve_id %d" % curve_id)
if (len(qx) != curve_size or len(qy) != curve_size
or len(d) != curve_size):
raise ValueError(
"qx, qy and d must each be %d bytes for curve_id %d, got "
"qx=%d qy=%d d=%d" % (curve_size, curve_id,
len(qx), len(qy), len(d)))
ret = _lib.wc_ecc_import_unsigned(self.native_object, qx, qy, d,
curve_id)
if ret != 0:
@ -1692,6 +1732,9 @@ if _lib.ED448_ENABLED:
if ctx is not None:
ctx_buf = t2b(ctx)
ctx_buf_len = len(ctx_buf)
if ctx_buf_len > 255:
raise ValueError("Ed448 ctx must be at most 255 bytes, got %d" %
ctx_buf_len)
ret = _lib.wc_ed448_verify_msg(signature, len(signature),
data, len(data), status,
@ -1812,6 +1855,9 @@ if _lib.ED448_ENABLED:
if ctx is not None:
ctx_buf = t2b(ctx)
ctx_buf_len = len(ctx_buf)
if ctx_buf_len > 255:
raise ValueError("Ed448 ctx must be at most 255 bytes, got %d" %
ctx_buf_len)
ret = _lib.wc_ed448_sign_msg(plaintext, len(plaintext),
signature, signature_size,

View File

@ -33,7 +33,7 @@ class Random:
A Cryptographically Secure Pseudo Random Number Generator - CSPRNG
"""
def __init__(self, nonce: __builtins__.bytes = b"", device_id: int = _lib.INVALID_DEVID) -> None:
def __init__(self, nonce: __builtins__.bytes = b"", device_id: int = -2) -> None:
self.native_object: _lib.RNG | None = _ffi.new("WC_RNG *")
ret = _lib.wc_InitRngNonce_ex(self.native_object, nonce, len(nonce), _ffi.NULL, device_id)