Skip to content

Gate FFI declarations and add key validation for optional wolfSSL build configurations - #144

Open
julek-wolfssl wants to merge 26 commits into
wolfSSL:masterfrom
julek-wolfssl:fenrir/20260924
Open

julek-wolfssl wants to merge 26 commits into
wolfSSL:masterfrom
julek-wolfssl:fenrir/20260924

Conversation

@julek-wolfssl

Copy link
Copy Markdown
Member

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.py is split into detect_features(), make_source(),
    make_cdef() and default_features(), with main() running only when
    invoked directly or via cffi's setuptools hook. Feature detection now uses
    a defined() helper so indentation, values, trailing comments and
    last-line macros are all matched correctly, including for
    WC_RSA_BLINDING, ECC_TIMING_RESISTANT and the ML-DSA no-context macros.
  • Each optional capability is detected on its own and the corresponding FFI
    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 NotImplementedError instead of failing to import.
  • ECC and ECDSA hardening:
    • EccPublic and EccPrivate now validate imported keys with
      wc_ecc_check_key(), rejecting points that are off-curve, out of
      range, or of the wrong order.
    • EccPrivate.encode_key() sizes its DER buffer with wc_EccKeyDerSize
      instead of a fixed multiple of the field size, fixing BAD_FUNC_ARG on
      curves like P-192.
    • sign(), verify(), sign_raw() and verify_raw() now check that the
      input is a valid digest size before calling into wolfSSL.
  • ChaCha's set_iv() now clears the IV-set state before validating the
    nonce length, so a rejected nonce blocks subsequent encrypt()/decrypt()
    calls instead of leaving the previous IV in effect.
  • Build fixes: the bundled wolfSSL is now built with -fPIC on every Linux
    platform, not just x86, and CI now also runs on arm64 Linux.

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.
Copilot AI lite review requested due to automatic review settings September 28, 2026 16:48
@julek-wolfssl julek-wolfssl self-assigned this Sep 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved FFI capability-gating and no-RNG API issues block approval.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

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.

Comment thread scripts/build_ffi.py
Comment thread wolfcrypt/ciphers.py
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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants