Discovered while wrapping wolfCrypt behind safe Rust in the wolfcrypt-rs and
wolfcrypt crates. Each gap required either a workaround in Rust or a shim
function in wolfcrypt-rs/src/compat_shim.c.
The gaps are grouped into three categories: confirmed bugs in wolfSSL's C API, missing API surface (functionality that should be in wolfSSL but isn't), and API design quirks that are not wrong per se but impose friction on FFI wrappers.
Affected function: wolfSSL_d2i_ECPrivateKey
RFC 5915 DER encodings of EC private keys include an optional publicKey
field. When that field is absent, OpenSSL derives the public point from the
private scalar automatically. wolfSSL instead sets type = ECC_PRIVATEKEY_ONLY
and leaves the public point uninitialized. Any subsequent operation that needs
the public key — ECDSA sign, ECDH, key export — fails or produces garbage.
Workaround in compat_shim.c: wolfcrypt_fix_ec_privatekey_only() detects
ECC_PRIVATEKEY_ONLY, calls wc_ecc_make_pub() to derive the public point
from the private scalar, resets type = ECC_PRIVATEKEY, then syncs the OpenSSL
compat layer via the internal SetECKeyExternal(). Called unconditionally
after every d2i_ECPrivateKey import.
Fix: wolfSSL_d2i_ECPrivateKey should call wc_ecc_make_pub when the
publicKey field is absent, matching OpenSSL behavior.
Filed: Zendesk #21732
Affected function: wolfSSL_EVP_CIPHER_iv_length
EVP_CIPHER_iv_length(EVP_aes_128_cfb128()) — and the 192-bit and 256-bit
variants — returns 0. The correct IV length for AES-CFB128 is 16 bytes.
This causes callers that derive IV size from the cipher descriptor to produce
zero-length IVs, silently corrupting every encryption.
Workaround in wolfcrypt-ring-compat: A wrapper function
evp_cipher_iv_length() (streaming.rs:690) checks for a 0 return and, if the
cipher pointer matches one of the three CFB128 descriptors, substitutes 16.
/// EVP_CIPHER_iv_length with wolfSSL CFB128 bug workaround.
/// wolfSSL incorrectly returns 0 for CFB128 cipher IV lengths.
unsafe fn evp_cipher_iv_length(cipher: *const EVP_CIPHER) -> c_int {
let len = EVP_CIPHER_iv_length_raw(cipher);
if len == 0 && (cipher == EVP_aes_128_cfb128() || ...) {
return 16;
}
len
}Fix: The CFB128 cipher descriptors in wolfSSL should report iv_len = 16.
Filed: Zendesk #21730
Affected functions: wolfSSL_EVP_DigestSignUpdate, wolfSSL_EVP_DigestVerifyUpdate
OpenSSL declares both functions with size_t cnt. wolfSSL declares
DigestSignUpdate with unsigned int cnt and DigestVerifyUpdate with
size_t cnt. The inconsistency means a Rust binding (or any caller) cannot
use the same type for both and must special-case DigestSignUpdate.
Workaround in wolfcrypt-rs/src/lib.rs:975: The FFI declarations match
wolfSSL's inconsistency exactly, with a comment noting it is a wolfSSL bug:
// NOTE: wolfSSL declares SignUpdate with `unsigned int cnt` but
// VerifyUpdate with `size_t cnt`. OpenSSL uses `size_t` for both.
// This is a wolfSSL bug; the mismatch below matches upstream as-is.
Fix: Change wolfSSL_EVP_DigestSignUpdate's cnt parameter to size_t.
Filed: Zendesk #21734
Affected function: wolfSSL_ERR_error_string
This function calls SetErrorString, which is defined in internal.c — the
TLS handshake state machine, over 30,000 lines. A crypto-only build that does
not compile internal.c gets a linker error for this single symbol, even
though SetErrorString has no dependency on TLS state.
Workaround in compat_shim.c:392: A __attribute__((weak)) stub converts
the numeric error code to a decimal string without calling any wolfSSL code.
The weak attribute ensures that if a downstream binary links the full wolfSSL
(including internal.c), the real implementation wins and the stub is
discarded.
The comment reads: "TODO: upstream wolfSSL issue to decouple SetErrorString from internal.c so crypto-only builds don't need this stub."
Fix: Move SetErrorString (or its error-code-to-string mapping) out of
internal.c into a file compiled for all wolfSSL configurations.
Filed: Zendesk #21735
Missing function: no equivalent of AES_wrap_key_padded / AES_unwrap_key_padded
wolfSSL provides AES_wrap_key / AES_unwrap_key for RFC 3394 standard key
wrap. RFC 5649 (padded key wrap, supporting non–block-multiple plaintext
lengths) is not implemented.
The RFC 5649 multi-block unwrap path also cannot use wolfSSL's AES_unwrap_key
even as a building block, because wolfSSL's implementation validates the
recovered A register against a caller-supplied IV before returning. RFC 5649
requires recovering the AIV — which encodes the plaintext length — and then
validating it; wolfSSL's validation happens first and rejects the AIV. The
entire RFC 3394 unwrap loop had to be reimplemented from scratch on top of
AES-ECB.
Workaround in compat_shim.c:443: Full RFC 5649 wrap and unwrap are
implemented using wolfSSL's wolfSSL_AES_ecb_encrypt (single-block case) and
wolfSSL_AES_wrap_key with a custom IV (multi-block wrap). Multi-block unwrap
reimplements the RFC 3394 loop using AES-ECB to recover the AIV before
validating it.
Fix: Add native wolfSSL_AES_wrap_key_padded / wolfSSL_AES_unwrap_key_padded
per RFC 5649.
Filed: Zendesk #21736
Missing function: HMAC-based KBKDF per NIST SP 800-108r1 §4.1
wolfSSL provides wc_KDA_KDF_PRF_cmac for CMAC-based key-based key derivation
(SP 800-108) but has no HMAC variant. The HMAC variant is required for
interoperability with systems (TLS, SSH, JOSE) that specify KBKDF-CTR-HMAC
rather than KBKDF-CTR-CMAC.
Workaround in compat_shim.c:792: KBKDF_ctr_hmac() implements the
NIST SP 800-108r1 §4.1 counter-mode KDF using wolfSSL's WOLFSSL_HMAC_CTX
primitives directly: per-iteration counter as a 32-bit big-endian prefix,
concatenated with the caller-supplied FixedInfo, hashed with HMAC.
Fix: Add a native wc_KBKDF_ctr_hmac function (or extend wc_KDA_KDF_PRF
to accept a MAC type selector) matching the existing CMAC variant.
Filed: Zendesk #21737
Affected function: wc_ChaCha20Poly1305_Final
wolfCrypt's streaming ChaCha20-Poly1305 API (Init / UpdateAad /
UpdateData / Final) tracks a state machine (READY → AAD → DATA → DONE).
Final requires the state to be AAD or DATA, not READY. When both AAD and
plaintext are empty — a valid case per RFC 8439 — neither UpdateAad nor
UpdateData is called, the state remains READY, and Final returns
BAD_STATE_E.
Workaround in wolfcrypt/src/aead.rs:290: UpdateData is called
unconditionally with a zero-length buffer (using a stack sentinel pointer
because empty Rust slices may have dangling as_ptr() values). This
transitions state to DATA without processing any bytes, which is correct per
RFC 8439.
// We always call UpdateData even when buffer is empty because
// wolfCrypt's state machine requires at least one UpdateAad or
// UpdateData call before Final (state must be AAD or DATA, not
// READY).Fix: wc_ChaCha20Poly1305_Final should accept READY state as valid when
both AAD length and data length are zero, or the state machine should advance
to DATA implicitly when Init is called.
Filed: GitHub issue #10040, PR #10046
Affected function: wc_curve25519_make_pub
RFC 7748 §5 specifies that Curve25519 private keys must be clamped (clear bits
0, 1, 2 of byte 0; clear bit 7 of byte 31; set bit 6 of byte 31) before use.
wolfSSL's wc_curve25519_make_pub requires that clamping has already been
applied by the caller. It does not clamp internally. Passing an unclamped
scalar produces a wrong public key without returning an error.
Workaround in wolfcrypt/src/ecdh.rs:64: The clamping step is performed
explicitly before calling wc_curve25519_make_pub:
// Clamp the private scalar per RFC 7748 Section 5.
// wolfSSL requires clamped keys for `wc_curve25519_make_pub`.
let mut clamped = *private;
clamp(&mut clamped);Fix: wc_curve25519_make_pub should clamp internally, matching the
behavior of every other Curve25519 implementation. Alternatively, the
documentation should state this requirement explicitly.
Filed: Zendesk #21731
Affected function: wc_curve25519_shared_secret_ex
wolfSSL enables Curve25519 scalar-multiplication blinding by default. Blinding
requires an RNG to be attached to the private-key struct via
wc_curve25519_set_rng() before any scalar multiply. The DH API
(wc_curve25519_shared_secret_ex) does not accept an RNG parameter inline.
This forces every DH operation to initialise a throwaway RNG, set it on the
key, perform the multiply, and then free the RNG — four extra steps per
operation, one of which (wc_InitRng) may touch the entropy source.
Workaround in wolfcrypt/src/ecdh.rs:157:
// wolfSSL enables Curve25519 blinding by default, which requires
// an RNG attached to the private key for scalar multiplication.
// Create a temporary RNG for this operation.
let mut rng = WC_RNG::zeroed();
wc_InitRng(&mut rng);
wc_curve25519_set_rng(&mut self.key, &mut rng);
// ... wc_curve25519_shared_secret_ex ...
wc_FreeRng(&mut rng);Fix: Accept an optional WC_RNG * parameter in wc_curve25519_shared_secret_ex
(and the make-public equivalent), so callers can pass their existing RNG rather
than constructing a temporary one.
Filed: Zendesk #21738
Affected functions: wc_Ed25519Sign, wc_Ed25519Verify, wc_Ed448Sign,
wc_Ed448Verify, wc_ecc_sign_hash, wc_ecc_verify_hash, and their RSA,
ML-DSA, and LMS equivalents.
wolfCrypt's C API takes *mut key for operations that are logically read-only,
including signature verification and public-key access. This is a common
pattern in C ("const-correctness debt") but it breaks straightforwardly in Rust:
the signature::Verifier trait takes &self, and there is no safe way to
obtain *mut from &self without interior mutability.
Workaround in wolfcrypt/src/: Every key type that backs a sign or verify
operation wraps its C key handle in UnsafeCell, which allows obtaining *mut
from &self for FFI calls. The UnsafeCell makes the type !Sync, which is
correct — wolfCrypt key handles are not safe to share across threads.
Affected types: Ed25519SigningKey, Ed25519VerifyingKey, Ed448SigningKey,
Ed448VerifyingKey, EcdsaSigningKey, EcdsaVerifyingKey, RsaPrivateKey,
RsaPublicKey, MlDsa*SigningKey, MlDsa*VerifyingKey.
Fix: Mark key parameters const in the C signatures of verification and
public-key-export functions where the key is not actually modified. This is
routine C const-correctness work.
Filed: Zendesk #21739 (covers bugs 10 and 11)
Affected functions: wc_Ed448PrivateKeyDecode, wc_Ed448PrivateKeyToDer,
wc_Ed448KeyToDer
The equivalent Ed25519 functions (wc_Ed25519PrivateKeyDecode,
wc_Ed25519PrivateKeyToDer, wc_Ed25519KeyToDer) take const * key
pointers where the operation is read-only. The Ed448 versions take *mut.
The asymmetry is unexplained; neither set of operations modifies the key.
Workaround in wolfcrypt-rs/src/lib.rs:1949: The FFI declarations match
wolfSSL's inconsistency and a comment explains the discrepancy:
// NOTE: Ed448 DER functions take non-const key pointers, unlike their
// Ed25519 counterparts which take `const`. This matches the upstream
// wolfSSL API — the inconsistency is in wolfSSL itself.
Fix: Change wc_Ed448PrivateKeyToDer and wc_Ed448KeyToDer to accept
const wc_ed448_key * where the key is not modified.
Filed: Zendesk #21739 (covers bugs 10 and 11)
Affected function: wc_curve25519_make_pub
The signature is:
int wc_curve25519_make_pub(int pubSz, byte *pub_, int privSz, const byte *priv_);Output parameters (pubSz, pub_) come before input parameters (privSz,
priv_). The rest of the wolfCrypt API follows the conventional C ordering
of input before output. This is a readability hazard: callers can silently
swap arguments and get no compile error because both size parameters are int.
Workaround: The FFI binding in wolfcrypt-rs/src/lib.rs:1972 carries an
explicit comment:
// NOTE: wolfSSL signature has output params before input params
Fix: Swap the parameter order in a future wolfSSL major release, or
introduce a correctly-ordered wc_curve25519_make_public_key alias.
Filed: Zendesk #21733
Affected structs: Aes, WC_RNG, Poly1305, ChaCha, ChaChaPoly_Aead,
ed25519_key, curve25519_key, ed448_key, curve448_key, dilithium_key,
LmsKey, XtsAes, Hpke, and others.
Rust cannot know the layout of wolfSSL C structs without running bindgen for
every build configuration. bindgen output is fragile — struct sizes change
with WOLF_CRYPTO_CB (adds devId, devCtx, devKey fields to key types),
platform word size, and enabled algorithm sets.
Workaround in wolfcrypt-rs: Each struct is stack-allocated as a
[u8; N] blob with a hand-chosen N that is rounded up to a safe
over-approximation. compat_shim.c:82–206 contains _Static_assert checks
that fire at compile time if the actual struct outgrows the allocation:
_Static_assert(sizeof(Aes) <= 512,
"Aes exceeds WC_AES_ALLOC_SIZE (512) in lib.rs");
_Static_assert(_Alignof(Aes) <= 16,
"Aes alignment exceeds repr(C, align(16)) in lib.rs");When a wolfSSL upgrade changes a struct's size or alignment, the build fails
with a diagnostic, and only compat_shim.c and the corresponding constant in
lib.rs need updating.
Fix: wolfSSL could provide stable C accessor functions and heap-allocate
opaque handle types (returning Aes * rather than requiring Aes in the
caller's stack frame). Alternatively, exporting a wc_AesSize() function
that returns sizeof(Aes) at runtime would let the Rust side allocate the
right amount without a compile-time constant.
Filed: Zendesk #21740