Gate FFI declarations and add key validation for optional wolfSSL build configurations - #144
Open
julek-wolfssl wants to merge 26 commits into
Open
julek-wolfssl wants to merge 26 commits into
julek-wolfssl wants to merge 26 commits into
Conversation
set_iv() now validates the nonce before storing it and clears the IV-set state first, so any failed set_iv() blocks encrypt()/decrypt() until a valid nonce is set.
EccPublic.decode_key_raw() accepted any point of the right length because wc_ecc_import_unsigned() does not check it. decode_key_raw(), import_x963() and decode_key() now call wc_ecc_check_key() and raise WolfCryptApiError when the point is not on the curve, is out of range, or has the wrong order. wolfSSL 5.9.0 and later already check X9.63 and DER imports; older local builds do not.
EccPrivate.encode_key() allocated four times the curve field size for the DER output. On curves up to P-192 the encoding needs more than that (97 bytes for P-192, which FIPS builds enable), so wc_EccKeyToDer returned BAD_FUNC_ARG. Query the exact length with wc_EccKeyDerSize.
build_ffi.py declared wc_AesCtrEncrypt whenever AES was enabled, but wolfSSL only provides it with WOLFSSL_AES_COUNTER. Against a local wolfSSL without AES-CTR, the extension then failed to import with an undefined symbol. Detect WOLFSSL_AES_COUNTER, expose AES_CTR_ENABLED, and declare wc_AesCtrEncrypt only when it is set. Aes now raises NotImplementedError for MODE_CTR when AES-CTR is not compiled in. To make detection and cdef generation testable, split the build script into detect_features(), make_source(), make_cdef() and default_features(). main() now runs only when the script runs directly or through cffi's setuptools hook. get_platform falls back to sysconfig when distutils is unavailable. For the bundled options.h and both Windows user_settings.h files, the generated source and cdef are unchanged except for the new flag.
wolfSSL declares wc_AesCbcEncrypt/wc_AesCbcDecrypt only with HAVE_AES_CBC and builds CBC decryption only with HAVE_AES_DECRYPT. settings.h sets both unless NO_AES_CBC or NO_AES_DECRYPT is defined. Against a local wolfSSL built with either of those, the extension failed to import with an undefined symbol. Detect both, expose AES_CBC_ENABLED and AES_DECRYPT_ENABLED, and gate the declarations. Aes now raises NotImplementedError for MODE_CBC without AES-CBC and from CBC decrypt() without AES decryption. CTR decryption is unaffected.
wolfSSL builds wc_AesGcmDecryptInit/Update/Final only with HAVE_AES_DECRYPT or HAVE_AESGCM_DECRYPT. Against a local wolfSSL with WOLFSSL_AESGCM_STREAM and NO_AES_DECRYPT, the extension failed to import with an undefined symbol. Detect this, expose AESGCM_STREAM_DECRYPT_ENABLED, and declare the streaming decrypt functions only when they are available. AesGcmStream.decrypt() now raises NotImplementedError when streaming decryption is not compiled in. Encryption is unaffected.
wolfSSL builds each SHA-3 size unless WOLFSSL_NOSHA3_224/256/384/512 is defined, and settings.h keeps only SHA3-384 on Xilinx. Against a local wolfSSL with one of those sizes disabled, the extension failed to import with an undefined symbol. Detect each size, expose SHA3_<bits>_ENABLED, and declare the SHA-3 functions only for the sizes that are available. Sha3 raises NotImplementedError for a size that is not compiled in, before allocating the state. Invalid sizes are unchanged.
wolfSSL builds wc_HKDF* only without NO_HMAC, so a HAVE_HKDF build with --disable-hmac no longer declares the HKDF functions. The HKDF tests import the module only when HKDF is enabled.
wolfSSL builds wc_PBKDF2 only with HAVE_PBKDF2 and without NO_HMAC. Detect a PBKDF2 capability from PWDBASED, HMAC and the settings.h HAVE_PBKDF2 derivation, and use it for the wc_PBKDF2 declaration and wolfcrypt.pwdbased.PBKDF2. A pwdbased build with --disable-hmac now builds and imports.
wolfSSL builds wc_PemToDer only with WOLFSSL_PEM_TO_DER and wc_DerToPemEx only with WOLFSSL_DER_TO_PEM. settings.h derives these macros, and a plain --disable-keygen build has no DER-to-PEM. Detect each direction on its own, declare the functions only when they exist, and define pem_to_der, der_to_pem and the RSA from_pem classmethods only when the matching direction is available.
asn.c builds wc_GetPkcs8TraditionalOffset only with HAVE_PKCS8 and without NO_ASN. settings.h defines HAVE_PKCS8 unless both NO_PKCS8 and NO_PKCS12 are set. Detect PKCS8 from those macros, declare the helper only when it exists, and skip the PKCS#8 fallback in RsaPrivate when it is absent. wc_RsaPrivateKeyDecode already skips a PKCS#8 header itself when PKCS#8 support is built.
With WC_NO_RNG, random.h turns the RNG API into macros (wc_FreeRng becomes a void expression), and random.c and rsa.c build no DRBG, seed callback or wc_RsaSetRNG functions, so the extension did not compile. Detect RNG from WC_NO_RNG, turn off HASHDRBG, WC_RNG_SEED_CB and RSA_BLINDING without it, and declare the RNG functions only when it is present. Random() raises NotImplementedError on such builds, so RSA keys and default RNG arguments are unusable there; tests that need an RNG are skipped.
wolfSSL leaves out RSA operations with WOLFSSL_RSA_PUBLIC_ONLY (--enable-rsapub), WOLFSSL_RSA_VERIFY_ONLY and WOLFSSL_RSA_VERIFY_INLINE (--enable-rsavfy) and WC_NO_RSA_OAEP (--disable-oaep). Against such a local wolfSSL the extension failed to import with an undefined symbol. Detect RSA_ENCRYPT, RSA_PRIVATE, RSA_SIGN, RSA_VERIFY and RSA_OAEP and declare each operation only when it is built. encrypt, encrypt_oaep, decrypt, decrypt_oaep, make_key and sign_pss are defined only when their operations exist. RsaPrivate.sign() and RsaPublic.verify() raise NotImplementedError instead.
wolfSSL leaves out ECC signing, verification, ECDH, key import and key export with NO_ECC_SIGN, NO_ECC_VERIFY, NO_ECC_DHE, NO_ECC_KEY_IMPORT and NO_ECC_KEY_EXPORT. settings.h also drops ECDH, and signing with ECC_TIMING_RESISTANT, when there is no RNG, and key export without SP or big integer math. Against such a local wolfSSL the extension failed to import with an undefined symbol. Detect ECC_SIGN, ECC_VERIFY, ECC_DHE, ECC_KEY_IMPORT and ECC_KEY_EXPORT and declare each operation only when it is built. EccPublic and EccPrivate methods are defined only when their operation exists, and creating a key object from a key raises NotImplementedError without key import.
EccPrivate.decode_key_raw() and decode_key() accepted a public point that is not on the curve, as the EccPublic imports did before F-8277. Check the key after import. decode_key() checks only keys that include the public point, since it is optional in an ECPrivateKey and wc_ecc_check_key() rejects a key without it.
wolfSSL leaves out Ed25519 key generation, signing, verification, key import and key export with NO_ED25519_MAKE_KEY, NO_ED25519_SIGN, NO_ED25519_VERIFY, NO_ED25519_KEY_IMPORT and NO_ED25519_KEY_EXPORT. Against such a local wolfSSL the extension failed to import with an undefined symbol. Detect each operation and declare it only when it is built. Ed25519Public and Ed25519Private methods are defined only when their operation exists. Creating a key object from a key raises NotImplementedError without key import, and decoding a private key without its public key raises NotImplementedError without key generation, since the public key is derived with wc_ed25519_make_public.
wolfSSL leaves out Ed448 signing, verification, key import and key export with NO_ED448_SIGN, NO_ED448_VERIFY, NO_ED448_KEY_IMPORT and NO_ED448_KEY_EXPORT. Against such a local wolfSSL the extension failed to import with an undefined symbol. Detect each operation and declare it only when it is built. Ed448Public and Ed448Private methods are defined only when their operation exists. Creating a key object from a key raises NotImplementedError without key import. Key generation and decoding a private key without its public key still work, since wolfSSL always builds wc_ed448_make_key and wc_ed448_make_public.
wolfSSL leaves out ML-KEM key generation, encapsulation and decapsulation with WOLFSSL_MLKEM_NO_MAKE_KEY, WOLFSSL_MLKEM_NO_ENCAPSULATE and WOLFSSL_MLKEM_NO_DECAPSULATE (or the legacy WOLFSSL_KYBER_NO_* names), which --enable-mlkem=...,enc and similar configure options set. Against such a local wolfSSL the extension failed to import with an undefined symbol. Detect each operation and declare it only when it is built. MlKemPrivate.make_key(), make_key_with_random() and decapsulate(), and MlKemPublic.encapsulate() and encapsulate_with_random() are defined only when their operation exists. Key setup, sizes, encoding and decoding are always built and stay available.
wolfSSL leaves out ML-DSA key generation, signing and verification with WOLFSSL_MLDSA_NO_MAKE_KEY, WOLFSSL_MLDSA_NO_SIGN, WOLFSSL_MLDSA_NO_VERIFY and WOLFSSL_MLDSA_VERIFY_ONLY (or the legacy WOLFSSL_DILITHIUM_* names), which --enable-mldsa=...,verify-only and similar configure options set. Public and private key import and export follow from these. Against such a local wolfSSL the extension failed to compile or import with an undefined symbol. Detect each operation and key part and declare it only when it is built. MlDsaPrivate and MlDsaPublic methods are defined only when the operations they need exist. MlDsaPrivate.decode_key() raises NotImplementedError when given a public key that the build cannot import.
The no-context ML-DSA macros were matched with a regex that required whitespace after the name, so a macro on the last line of the parsed options.h or user_settings.h was missed and ML_DSA_NO_CTX stayed 0. Use the defined() helper, which ends the name at a word boundary.
detect_features() found most features by comparing whole lines with '#define NAME', so a define that was indented, had a value or had a trailing comment was missed. For WC_RSA_BLINDING and ECC_TIMING_RESISTANT, the RSA keys and EccPrivate.make_key() then did not call wc_RsaSetRNG() and wc_ecc_set_rng(). Use the defined() helper for all of them. The bundled options.h and both Windows user_settings.h files give the same features as before.
EccPrivate.sign() and EccPublic.verify() pass their input to wc_ecc_sign_hash() and wc_ecc_verify_hash(), which expect a message digest. Raise ValueError when the input is not the size of a SHA-1 or SHA-2 digest that this wolfSSL build accepts, as bounded by WC_MIN_DIGEST_SIZE and WC_MAX_DIGEST_SIZE. Callers keep passing the digest as before. build_ffi.py declares both bounds for every build, including ones without RSA.
sign_raw() and verify_raw() pass their input to wc_ecc_sign_hash_ex() and wc_ecc_verify_hash_ex() as a digest. Raise ValueError when it is not the size of a SHA-1 or SHA-2 digest, as sign() and verify() do.
The static library is linked into the shared CFFI extension. Only x86 Linux got -fPIC, so linking failed on other architectures such as aarch64.
The build job builds the bundled static wolfSSL and links it into the CFFI extension, so on ubuntu-24.04-arm it fails unless wolfSSL is compiled with -fPIC.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved FFI capability-gating and no-RNG API issues block approval.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
This PR adds capability-gated wolfSSL FFI/Python APIs, ECC validation, ChaCha IV hardening, and improved build portability.
Changes:
- Detects optional wolfSSL features and gates declarations and APIs.
- Adds ECC key and digest validation plus DER sizing fixes.
- Expands regression tests and arm64 Linux CI coverage.
| File | Description |
|---|---|
wolfcrypt/random.py |
RNG availability handling |
wolfcrypt/pwdbased.py |
PBKDF2 gating |
wolfcrypt/hashes.py |
Per-variant SHA-3 gating |
wolfcrypt/ciphers.py |
Capability gating, ECC hardening, and ChaCha updates |
wolfcrypt/asn.py |
Independent PEM/DER capability gates |
wolfcrypt/_ffi/lib.pyi |
Feature flags and API declarations |
tests/test_random.py |
RNG regression coverage |
tests/test_pwdbased.py |
PBKDF2 coverage |
tests/test_mlkem.py |
ML-KEM coverage |
tests/test_mldsa.py |
ML-DSA coverage |
tests/test_hkdf.py |
HKDF coverage |
tests/test_hashes.py |
Hash capability coverage |
tests/test_delete_descriptor_binding.py |
Descriptor binding coverage |
tests/test_ciphers.py |
Cipher and key API coverage |
tests/test_chacha_iv.py |
ChaCha IV-state regression coverage |
tests/test_build_ffi.py |
Feature detection and FFI generation coverage |
tests/test_asn.py |
ASN capability coverage |
tests/test_aesgcmstream.py |
AES-GCM streaming coverage |
scripts/build_ffi.py |
Feature detection and conditional FFI generation |
.github/workflows/python-app.yml |
arm64 Linux CI coverage |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
RsaPublic/RsaPrivate no longer build a Random in __init__ unless blinding needs it. Verify now works in WC_NO_RNG builds; encrypt and sign still raise NotImplementedError there.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


This PR makes the extension buildable and importable against wolfSSL builds
that omit optional features, instead of failing at import time with an
undefined symbol or at compile time.
build_ffi.pyis split intodetect_features(),make_source(),make_cdef()anddefault_features(), withmain()running only wheninvoked directly or via cffi's setuptools hook. Feature detection now uses
a
defined()helper so indentation, values, trailing comments andlast-line macros are all matched correctly, including for
WC_RSA_BLINDING,ECC_TIMING_RESISTANTand the ML-DSA no-context macros.declarations and Python methods are only exposed when wolfSSL actually
builds them: AES-CTR, AES-CBC (encrypt/decrypt), streaming AES-GCM
decryption, SHA-3 (per size), HKDF, PBKDF2, PEM-to-DER/DER-to-PEM,
PKCS#8, RNG, RSA (encrypt/private/sign/verify/OAEP), ECC
(sign/verify/DHE/key import/export), Ed25519, Ed448 and ML-KEM/ML-DSA
operations. Where an operation is unavailable, the corresponding Python
method now raises
NotImplementedErrorinstead of failing to import.EccPublicandEccPrivatenow validate imported keys withwc_ecc_check_key(), rejecting points that are off-curve, out ofrange, or of the wrong order.
EccPrivate.encode_key()sizes its DER buffer withwc_EccKeyDerSizeinstead of a fixed multiple of the field size, fixing
BAD_FUNC_ARGoncurves like P-192.
sign(),verify(),sign_raw()andverify_raw()now check that theinput is a valid digest size before calling into wolfSSL.
set_iv()now clears the IV-set state before validating thenonce length, so a rejected nonce blocks subsequent
encrypt()/decrypt()calls instead of leaving the previous IV in effect.
-fPICon every Linuxplatform, not just x86, and CI now also runs on arm64 Linux.