wolfSSL_X509_get_pubkey() built a new WOLFSSL_EVP_PKEY, including a
freshly decoded RSA or EC key, on every call, and X509_get0_pubkey()
mapped to the same function. Callers following the OpenSSL get0
contract, which returns a pointer owned by the certificate, leaked the
whole decoded key graph on each call.
Decode the public key once into the certificate's existing
WOLFSSL_X509_PUBKEY member, lazily on first use (OPENSSL_ALL already
fills it at parse time), and free it with the certificate.
wolfSSL_X509_get_pubkey() now returns that key with a new reference,
matching X509_get_pubkey(). Add wolfSSL_X509_get0_pubkey(), which
returns it without a reference, and map X509_get0_pubkey() and
X509_REQ_get0_pubkey() to it. Add wolfSSL_X509_PUBKEY_get0()
(X509_PUBKEY_get0). wolfSSL_X509_get_X509_PUBKEY() decodes the key too,
so X509_PUBKEY_get() and X509_PUBKEY_get0_param() work outside
OPENSSL_ALL. get0_param no longer dereferences a missing key and maps
the stored key OID to a NID before creating the algorithm object. A
certificate without a public key does not cache an empty key.
The lazily decoded key is published with a compare-and-exchange, the
same way the context private key cache is, so concurrent first calls on
a shared certificate do not leak a key. The EC public point's internal
copy is marked as set after SetECKeyExternal(), so readers of a shared
key do not rebuild it.
wolfSSL_X509_set_pubkey() and re-decoding a certificate drop the cached
key; set_pubkey keeps it when handed that very key, and refreshes the
key's algorithm OID, algorithm object and curve OID. The key.algor
member is now freed in every configuration that can allocate it, not
only OPENSSL_ALL.
Callers that freed the result of X509_get0_pubkey() must stop; the
ChangeLog records this.
SE050 and CryptoCell are supported with this.
WOLFSSL_NO_RSA_SW to disable the RSA software implementation.
WOLFSSL_NO_ECC_SW to disable the ECC software implementation.
wc_Sha3Update and wc_Sha3Final derived the hash type from the block count
only while sha3->hashType was WC_HASH_TYPE_NONE, then reused the cached
value for the life of the context.
InitSha3 clears the cache, and the software path runs it at the end of
every Final, so the stale value normally never escapes. A callback that
handles the Final returns before that reset, which leaves the cached type
in place for the next operation on the same wc_Sha3.
One context legitimately serves several variants: wc_Sha3 carries no
variant of its own, the caller picks it per call through p, and wolfCrypt's
own ML-KEM relies on that, running mlkem_hash512 and mlkem_hash256 over a
single object created by wc_InitSha3_256. With a crypto callback installed,
the first SHA3-512 pins the cache, and every later SHA3-256 reaches the
callback labelled SHA3-512. A callback that honours the type it is given
writes 64 bytes into the caller's 32-byte digest buffer; in the other
order it leaves half the buffer unwritten. Both were observed on a TC4Dx
HSM server offloading SHA-3 to the CSS hash engine: the overflow surfaced
as BUFFER_E out of wc_MlKemKey_MakeKey, the short write as a shared-secret
mismatch in decapsulation.
Derive the type from p on every call so the callback always describes the
operation being performed. The mapping moves into one helper shared by
wc_Sha3Update and wc_Sha3Final. The reset a callback-handled Final skips
is left alone; the callback owns the context in that path.
Verified on hardware: the TC4Dx SHA-3 offload now passes the full wolfHSM
client suite (SHA3-224/256/384/512 direct, async and DMA, ML-KEM 512/768/
1024 and ML-DSA 44/65/87) with the accelerator enabled, where it previously
failed at ML-KEM key generation.
Adds a crypto callback test that handles a SHA3-512 Final without
resetting the context, as a device does, then runs a SHA3-256 hash on the
same object and checks the variant the callback is told on both Update and
Final. It fails before this change.
The ML-KEM and ML-DSA device test also has its emulated device complete
SHA3-256 and SHA3-512, so ML-KEM's shared hash object alternates between
the two variants on the device, and the results must still match software.
Both algorithms do their hashing with SHAKE objects held inside the key,
and neither object kept the device id the key was created with, so a
registered crypto callback never saw any post-quantum hashing at all.
mlkem_prf_init() re-initialised the PRF with a hardcoded heap of NULL and
a device id of 0, discarding what mlkem_prf_new() had stored from
wc_MlKemKey_Init(). It runs at the top of key generation and of both
encapsulate paths, so the PRF and the matrix XOF that share that object
were software-only regardless of the key's device. The other resets of
that object, in mlkem_get_noise_i(), the matrix XOF absorb and the
decapsulation hash, re-initialised it with INVALID_DEVID as well, so the
binding was lost again after matrix generation or decapsulation.
mlkem_prf_init() becomes mlkem_prf_reset(), which returns the status and
resets the sponge through wc_Shake256_Reset(), keeping the heap, device id
and device context that mlkem_prf_new() stored. A full re-init would also
drop a device context the callback allocated on an earlier operation. The
three wc_mlkem.c call sites check its result, and every other reset of the
object goes through it or wc_Shake128_Reset(). The white-box tests that
used mlkem_prf_init() to construct a fresh PRF now call mlkem_prf_new().
The Intel decapsulation hash writes z and the start of the ciphertext
straight into the sponge before calling wc_Shake256_Update(), so a device
handling that update would only ever see the tail. It now takes that path
only when no callback can see the object, and otherwise hashes through
wc_Shake256_Update() like the portable code.
ML-DSA never bound its SHAKE object in the first place: wc_MlDsaKey_Init()
zeroed it along with the rest of the key and then discarded devId, and all
seven internal reset sites re-initialised with INVALID_DEVID. The key's
object is now initialised with the key's heap and device id, and every
reset goes through the same Reset functions. Since the object is now
initialised in USE_INTEL_SPEEDUP builds too, wc_MlDsaKey_Free() releases
it there as well.
Using the Reset functions rather than reading heap and devId back out of
the object also keeps PSOC6_HASH_SHA3 building, whose wc_Sha3 has neither
field.
Measured on x86 with a counting callback: ML-KEM-768 key generation went
from 0 SHAKE dispatches to 12, ML-DSA-44 key generation from 0 to 5, and
the share of Keccak permutations a device can observe rose from 2.9% to
20.6% for ML-KEM-768 key generation and from 0% to 13.2% for ML-DSA-65
signing. Encapsulate, decapsulate, sign and verify round trips are
unchanged, including rejection of a tampered signature.
Software-only builds see no behavioural change: without WOLF_CRYPTO_CB
there is no device to dispatch to.
With the device id kept, freeing these objects now reaches a
WOLF_CRYPTO_CB_FREE callback, carrying whichever type the last call left.
Matrix generation and ML-DSA's sampling of A leave the SHAKE objects typed
SHAKE-128, and ML-KEM's G() leaves its SHA-3 object typed SHA3-512, so
mlkem_prf_free(), mlkem_hash_free() and wc_MlDsaKey_Free() restore the
type each object was created with before freeing it. wc_MlDsaKey_Init()
can now also fail when its SHAKE object cannot be initialised, which its
documentation now lists.
Adds a regression test to cryptocb_test() with its own device, which
emulates hardware by keeping separate hash state for each object and
completing the SHAKE-256 calls itself. ML-KEM key generation, encapsulation
and decapsulation, including the implicit rejection path, and ML-DSA key
generation, signing and verification run on a key bound to that device
and on a software key with the same seeds, and every ciphertext, shared
secret and signature must match. The test fails if the device completed no
operation, which is the case before this change, and with
WOLF_CRYPTO_CB_FREE it checks the type each object is freed with. It
uses a dedicated callback because the shared myCryptoDevCb handles ML-KEM
key generation itself, in software with every device id cleared.
AuthEnvelopedData with more than one recipient never worked at all. That
decoder, unlike the EnvelopedData one, did not step over the recipients
left unread after the match, so the EncryptedContentInfo that follows the
set was parsed from the wrong offset. This is not specific to any
recipient type: two plain RSA recipients reproduce it.
The set start and size are now recorded there too, the whole set is
buffered before the search, and the skip is counted from where the
search stopped rather than from the head of the set, since the
recipients walked to get there are already accounted for.
tests/api/test_pkcs7.c gains test_wc_PKCS7_MultipleRecipients, which
encodes to two KeyTransRecipientInfo recipients and opens the message as
each of them in turn, for EnvelopedData and AuthEnvelopedData, whole and
fed in chunks. The second recipient is the interesting one: decoding as
the first would pass even with the search unbounded.
It also opens the message in chunks as a third identity that is none of
the recipients, which has to report PKCS7_RECIP_E. That identity is a
certificate from another CA, since a KeyTransRecipientInfo is matched on
the issuer hash alone and any certificate sharing an issuer with a
recipient would be taken for it.
The default CLIENT_SESSIONS_MULTIPLIER of 8 now falls back to 1 when
SESSION_ROWS times 8 would exceed the 65535 ClientCache row limit, so
--enable-titancache with --enable-session-cache-ref builds out of the box
instead of stopping at the CLIENT_SESSION_ROWS check. That is the same
geometry TITAN had when it forced NO_SESSION_CACHE_REF, so its persisted
cache layout is unchanged. An explicitly defined multiplier still has to
pass the size checks, and a value below 1 is now rejected at build time
rather than producing zero-sized arrays and a modulo by zero.
The ChangeLog entry now states that the multiplier scales both rows and
columns, so the new default shrinks the client cache 64x (2112 to 33
slots with the default cache size), and that it takes a positive integer
bounded by the 65535 row and column limits.
The wolfSSL_get_session() declaration now carries WC_DEPRECATED when the
cache reference path is compiled in, so an application is told at its own
call site rather than only when the library is built. It uses the same
condition as the build warning in ssl_sess.c, and
WOLFSSL_SESSION_CACHE_REF_WARNED silences both. Default builds are
untouched: SSL_get_session() maps to this function and OpenSSL does not
deprecate it, so a compatibility-layer application would otherwise warn,
and fail under -Werror, for a call that returns the safe session object.
TITAN_SESSION_CACHE no longer forces NO_SESSION_CACHE_REF, and asking for
the reference path with it is no longer rejected outright. The rule existed
because its 64937 session rows times the multiplier of 8 overflow the
ClientCache row limit, which the size checks in ssl_sess.h already enforce;
under the new default it could only override an explicit opt-in. Those
checks now name CLIENT_SESSIONS_MULTIPLIER, so a TITAN build that opts in
stops there and builds once the multiplier is lowered to 1. configure and
CMake drop their TITAN special cases to match.
Without NO_SESSION_CACHE_REF, wolfSSL_get_session() returns a reference into
the process-global client cache rather than a session object. That reference
is valid only while the slot it names still holds the session it was issued
for, which the caller cannot observe, so the safe use of the API is the one
the macro selects. Only the integration recipes defined it, and none of them
for that reason: --enable-opensslextra and --enable-opensslall, which is what
an application written against SSL_get_session() reaches for, left it unset.
settings.h now defines NO_SESSION_CACHE_REF unless WOLFSSL_SESSION_CACHE_REF
is defined, so every build selects it, including the user_settings.h, IDE and
bare-metal builds that have no configure step. Putting the rule in the header
rather than only in configure and CMake means no project has to add a macro to
keep the safe behaviour, and the ones that want the deprecated reference ask
for it instead. --enable-session-cache-ref and -DWOLFSSL_SESSION_CACHE_REF=yes
define that opt-in macro, and options.h records it alongside the negative one
so an application cannot disagree with the library it links.
A build that opts in warns at compile time and the warning names
WOLFSSL_SESSION_CACHE_REF_WARNED, which silences it while keeping the path. It
sits inside the NO_SESSION_CACHE region, so a build without a session cache
and a wolfCrypt-only build stay quiet, and it takes the #warning / #pragma
message form already used for WOLFSSL_KEYLOG_EXPORT_WARNED.
The recipes that define NO_SESSION_CACHE_REF on their own keep doing so and
still win over the option. Both build systems say so rather than leaving a
reported option disagreeing with the build: configure warns and clears its
summary line, CMake warns and syncs the cached option. configure also treats
TITAN_SESSION_CACHE as a conflict, because settings.h derives the macro from
it and that never reaches the compile line for a flag scan to find.
The os-check session-cache-ref entry existed because --enable-opensslextra
left the macro unset. It now asks for the reference path explicitly, or
test_wolfSSL_client_cache_id_overwrite would skip and the generation check
added with it would be covered nowhere.
Anything migrating has one lifetime difference to mind: the deprecated
reference stayed valid after wolfSSL_free(), the session object does not, so
code holding the result of wolfSSL_get_session() past the WOLFSSL it came from
has to move to wolfSSL_get1_session() and wolfSSL_SESSION_free().
NO_SESSION_CACHE_REF is documented as choosing what wolfSSL_get_session()
returns, but it also stopped AddSession() from writing the ClientCache. One
field, ssl->clientSession, is both the handle handed back and the
out-parameter whose presence makes AddSessionToCache() call
AddSessionToClientCache(), so suppressing the handle suppressed the write.
Nothing then populated that cache while wolfSSL_GetSessionClient() went on
searching it, and it is the only consumer, reached from wolfSSL_SetServerID()
with newSession 0. Resumption by server ID therefore never resumed in a build
defining the macro: no error, no log, just a full handshake every time. That
covers --enable-curl, --enable-nginx, --enable-all and the other
OpenSSL-compatibility recipes, all of which have defined it for years.
Gate the write on NO_CLIENT_CACHE, which is what declares the field, and leave
only the wolfSSL_get_session() return value behind NO_SESSION_CACHE_REF. No
code reads ssl->clientSession when the macro is defined, so filling it in is
invisible to callers. CLIENT_SESSIONS_MULTIPLIER stays at 1 under the macro:
its value of 8 exists to make a stale handle less likely to resolve, which
cannot happen when no handle is issued, so the footprint does not change.
The test drives the documented flow rather than the cache API, since the
existing client-cache tests call AddSessionToCache() themselves with a
non-NULL entry and so pass either way. It records a server ID before the
handshake, then asks a second connection on the same contexts to resume by
that ID, and checks the master secret and wolfSSL_session_reused(). An
uncached ID is the negative control. It is not gated on NO_SESSION_CACHE_REF
and runs in both configurations.
WC_ML_KEM_512 is 0, so a zeroed MlKemKey is indistinguishable from one
holding a valid ML-KEM-512 parameter set. Nothing could tell "not
initialised" from "initialised to the smallest parameter set", which is
why both DER decoders had to insist the caller pick the parameter set
first and then rejected any DER that named a different one.
MLKEM_FLAG_TYPE_SET records the difference explicitly. wc_MlKemKey_Init
sets it; a zeroed object does not have it. The two places that reset
flags after discarding key material now keep the bit, since the
parameter set survives a regenerate or a scrubbed decode.
Renumbering the parameter sets would have been the other way to get
here, and is not worth it: WC_ML_KEM_512 and KYBER512 are public, so
moving them silently changes which algorithm an already-compiled
application asks for, and the values are shared with wolfHSM,
wolfProvider and wolfPKCS11.
With the bit in hand, wc_MlKemKey_Init also takes WC_ML_KEM_TYPE_UNSET,
which sets the object up without committing to a parameter set.
wc_MlKemKey_PublicKeyDecode and wc_MlKemKey_PrivateKeyDecode then pass
ANONk to the ASN.1 layer, which already auto-detects, and adopt whatever
the algorithm OID names. A caller that has a certificate or a key file
and does not know its parameter set no longer has to read the OID out
first with wc_MlKemKey_TypeFromOidSum. A key initialised for a parameter
set behaves as before and still holds the DER to it.
A parameter set the build does not have is refused with NOT_COMPILED_IN
rather than adopted.
The README described how to regenerate the ML-KEM certificates in prose,
which amounts to "write a program". They are produced by
certs/renewcerts.sh instead, using the same OpenSSL 3.5 or newer binary
its ML-DSA section already requires.
An ML-KEM key cannot sign, so it can sign neither a certificate nor a
certificate request. Each certificate therefore comes from a throwaway
request carrying only the subject name, with x509 -req -force_pubkey
swapping in the ML-KEM public key before the ML-DSA-87 test CA signs it,
and keyUsage marked critical asserting keyEncipherment and nothing else,
per RFC 9935 Section 5.
The keys are written in the priv-only PKCS#8 shape, which is the RFC 9935
Section 6 expandedKey form: that decodes in a build without key
generation, which the seed form does not.
ML-KEM is probed separately from ML-DSA so a binary carrying one provider
and not the other still regenerates what it can.
The MLKEM_TYPE certificate generation paths had no coverage, so
mlkem_certgen_test drives wc_MakeCert_ex, wc_SignCert_ex and
wc_SetSubjectKeyIdFromPublicKey_ex for all three parameter sets. It signs
with ECDSA rather than ML-DSA, which is available in far more builds and
which the paths under test do not care about.
The peak of wc_falcon_make_key() was the solver's 28n byte scratch plus two
things that did not need to be live at the same time:
- falcon_compute_public() allocated 6n bytes of its own: twiddle tables built
at run time with modpow, and NTT(f). It runs before solve_NTRU, while the
solver's scratch sits idle, and the verifier already has the same tables as
constants. It now uses falcon_get_tables() and the verify NTT, works in that
idle scratch, and the mq_* helpers that built the tables are gone.
- wc_falcon_make_key() held h (2n bytes) across the whole generation. It now
passes no h to falcon_keygen() and computes h afterwards, once the solver's
scratch has been freed.
Peak heap goes from 21056 to 16960 bytes at Falcon-512 and from 41536 to 33344
at Falcon-1024 in every signer configuration. The generated keys are the same,
and key generation takes the same time.
The inversion of NTT(f) there was a per-coefficient modpow with word64 '%',
which on 32-bit targets can become a library division whose timing depends on
the secret operand. It is now the constant-time Barrett batch inversion that
the smallest memory signer already used, moved out of that signer so both
share it.
--enable-falcon=smallest-mem (WOLFSSL_FALCON_SIGN_SMALLEST_MEM, implies
small-mem) signs in a 20*n byte working area, following T. Pornin, "A
RAM-Efficient Implementation of Falcon" (IACR ePrint 2026/1915). Peak heap per
signature goes from 46KB to 12KB at Falcon-512 and from 92KB to 22KB at
Falcon-1024. With the default integer fpr backend signing is also about twice
as fast as small-mem (2.15ms to 1.03ms at Falcon-512 on an arm64 host); with
the native double backend it is about 1.5 times slower.
The signer never holds the ffLDL tree or a full basis in FFT form:
- ff* + gg* and Ff* + Gg* are computed exactly as integers, mod q and mod
p = 40961 with a CRT. q*p covers every key that decodes, so unlike the
paper's 18433 no key precondition or adjustment bitmap is needed.
- Self-adjoint polynomials are held at half size and transformed with the
paper's self-adjoint FFT, whose first stages stay on integers. The tree
is built during sampling from the half-size LDL, the right child of the
root is q^2/(ff* + gg*), and the node values are stored as L/(1 + X).
- Only the fractional part of each target, (cf mod q)/q and (-cF mod q)/q,
is sampled, and it is added at the leaves. The sampled integers are kept
and s2 = f*z0 + F*z1 is rebuilt mod p, so no inverse FFT runs at all.
- Each candidate is checked as a verifier would, s1 = c - s2*g/f, before
the norm test.
- Neither c nor the decoded basis is held. The sponge that absorbed
nonce || msg is kept (416 bytes) and c is squeezed again from a copy
wherever it is needed, which costs the same for any message length;
falcon_hash_to_point is split into absorb and squeeze halves for this.
f, g and F are decoded from the encoded key into the upper half of the
NTT buffer they are lifted into, and G is rebuilt once per attempt in the
idle second target buffer, or all four are read from the basis cache.
Together this saves 6n bytes for about 1% of signing time with the
integer fpr backend and 3-7% with native doubles. The copy is squeezed in
a non-inlined helper so that it is never on the stack under the sampler.
Algorithm 12 of the paper returns t - z at a leaf without the late-added
fraction e; it has to include it, or the left target misses the e1*L term
and no attempt is ever short.
G is recomputed with the mod-q NTT and a constant-time batch inversion rather
than falcon_complete_private, whose FFT scratch is larger than the whole
signing area. Keys whose f is not invertible mod q are rejected there.
The nonce and sampler seeding are unchanged, and the sampler is called in the
same order as by the other signers, so with the same randomness the signature
is the same except where rounding moves a sampler centre across a threshold:
2 of 10,000 signatures in a comparison against small-mem. The pinned
deterministic vectors are reproduced as they are.
The constant tables (R = 1/(2cos(phi)), C = 4cos(phi)^2, D = -tan(phi), and the
mod-p NTT twiddles) come from scripts/falcon_sm_tables.py, which rounds
correctly from Decimal arithmetic and, with --check, diffs its output against
falcon.c the way scripts/puf_bch_genpoly.py does for puf.c; the smallest-mem
CI legs run that check. Level-1-only builds keep only the first half of each
table.
The whitebox gains smallest-mem twins of the restart loop, sign_core error
latch and G recomputation tests; CI gains smallest-mem backend legs and runs
the liboqs interop for it as well.
falcon_native_check_key compared h*f against g slot by slot in the NTT
domain and broke at the first mismatch. ft[] and gt[] there are the NTT
images of the secret f and g, so the iteration count revealed the index of
the first slot that failed.
A consistent key always ran the loop to completion, so nothing leaked in
normal use. The exposure is an oracle: a caller able to import a chosen
public key against a stored private key, and to time the check, learns which
slot fails first, and by varying that public key can solve for the secret
one slot at a time.
Every slot is now examined and both conditions, f not invertible at the slot
and h*f != g at the slot, are folded into a sticky mask that is tested once
after the loop. Barrett's output and gt[] are both canonical in [0, q), so
XOR compares them exactly. Consistent keys, the case that matters for
performance, do exactly the work they did before.
Checked in the generated code as well as the source: the loop body holds no
conditional jump other than the induction-variable back edge, and the final
verdict compiles to a cmov.
test_wc_falcon_check_key already covered both directions, a matching pair
returning 0 and a mismatched pair returning PUBLIC_KEY_E, so the behaviour
is pinned.
wc_Falcon_PublicKeyDecode and wc_Falcon_PublicKeyToDer held the encoded
public key in a WC_DECLARE_VAR array, which outside WOLFSSL_SMALL_STACK is
1793 bytes of stack in functions on the certificate parsing and writing
paths. wc_Falcon_PrivateKeyDecode in the same file already allocates its
equivalent unconditionally; these now match it. All three pass key->heap
rather than NULL, and wc_Falcon_PrivateKeyDecode now wipes its decoded copy
of the private key before releasing it.
The buffer is also sized for the key's own level instead of the highest level
built, which is the size a valid encoding for that key has: 897 bytes rather
than 1793 for a Falcon-512 key.
Stack frames go from 1856 and 1840 bytes to 80 each. With this the largest
Falcon frames are verify at 4304 bytes, which is its arena, and signing at
912.
Three changes aimed at builds where a Falcon-1024-sized structure or an 8KB
stack frame is not affordable.
Levels are now selectable. --enable-falcon=level1 or =level5
(WOLFSSL_NO_FALCON_LEVEL5 / WOLFSSL_NO_FALCON_LEVEL1) builds only the levels
named; naming neither builds both, as before. Every FALCON_MAX_* bound
follows the highest level built rather than always Falcon-1024, so a level-1
only build takes falcon_key from 4120 to 2200 bytes and the verify stack
frame from 4304 to 2224. wc_falcon_set_level() rejects a level that was not
built.
--enable-falcon=dynamic-keys (WOLFSSL_FALCON_DYNAMIC_KEYS), the counterpart of
WOLFSSL_MLDSA_DYNAMIC_KEYS, moves the encoded public and private key to heap
buffers sized for the key's own level, leaving falcon_key at 32 bytes.
wc_falcon_set_level() allocates them, so it can now return MEMORY_E; they are
released, the private one zeroized, by wc_falcon_free() and on a level change.
The allocated length of the private buffer is kept in the key: deriving it
from key->level would zeroize a Falcon-1024 length over a Falcon-512
allocation if a caller had put an out-of-range level in the structure, which
the MC/DC coverage test does deliberately.
The verify working set drops from four n-element buffers to two, which is all
that is ever live at once. The public key is dead once the pointwise product
is formed, so the hashed point is built there instead and reuses it; the
signature polynomial is dead once its squared norm is accumulated, which now
happens before it is lifted in place into that product. wc_falcon_verify_msg
goes from an 8368-byte frame to 4304 at Falcon-1024, and to 2224 in a level-1
only build. Results are unchanged and WOLFSSL_SMALL_STACK still moves the
buffer to the heap.
wc_falcon_set_level() can therefore fail where it could not before, so its
callers check the result, and a failed reallocation leaves the key reading
as empty rather than with a level and no buffers.
The API tests, testwolfcrypt, the benchmark and the signature-group test
follow the levels the build has rather than both. CI runs the full unit
suite on every level-restricted and struct-changing leg, since
--group falcon reaches neither the signature group nor the TLS and ASN
callers of wc_falcon_set_level().
wc_falcon_sign_msg decoded the private key, recomputed G and rebuilt the
ffLDL tree on every call. With the default integer fpr backend that setup
is over half of each signature (54.8% at Falcon-512, 55.3% at Falcon-1024,
measured with timers around falcon_complete_private and
falcon_expand_privkey). Two per-key caches, both off by default and
modelled on the ML-DSA WC_MLDSA_CACHE_* options, let only the first
signature made with a key pay for it:
--enable-falcon=cache-key (WC_FALCON_CACHE_EXPANDED_KEY) holds the
expanded key, about 57KB at Falcon-512 and 120KB at Falcon-1024
--enable-falcon=cache-basis (WC_FALCON_CACHE_PRIV_BASIS) holds only the
secret basis, 4*n bytes, and is what cache-key reduces to in
small-mem builds; in the tree signer cache-key supersedes it, since
the expanded key already contains the basis
Signing with the emulated backend goes from 5.35ms to 2.56ms at Falcon-512
and 11.48ms to 5.57ms at Falcon-1024. Both caches hold secret material,
are zeroized when released, and are dropped on key generation (including
one serviced by a crypto callback), private-key import and level change.
Their lengths are recorded at allocation, so the release path does not
depend on key->level. Neither is synchronized. They are configure
sub-options rather than plain defines because they change struct
falcon_key, and a struct-changing define has to be recorded in
wolfssl/options.h.
The ffLDL root now uses det(G) = q^2, which holds for the NTRU basis, to
get D11 = q^2/G00 directly instead of the general D11 = G11 - L10*adj(G01).
The general form subtracts two values of about the same size to reach a
much smaller one and loses roughly ten bits of the mantissa to cancellation
at the root. The closed form loses none, reuses the reciprocal already
computed for L10, and needs no G11 at all, so neither signer forms it any
more. It stays scalar in every backend: the root is one pass over
2^(logn-1) points, and a vectorized twin would have to reproduce this
rounding exactly. Signatures for a given key and randomness differ from
before, which is not observable in use since signing is randomized and
verification is unchanged.
Adds test_wc_falcon_deterministic, which pins key generation and signing at
both levels by SHA-256, using WC_RNG_SEED_CB and the SHA-512 Hash_DRBG to
make the stream reproducible; configurations that bypass that DRBG are
skipped. The integer emulation, native double and asm fpr backends are
bit-exact with each other. AVX2 and NEON fuse multiply-add and reproduce
the digests empirically rather than by construction, so the NEON CI leg
builds with -ffp-contract=off. The emulated, asm, double, avx2 and
small-mem builds were confirmed byte-identical before the digests were
pinned. test_wc_falcon_key_reuse covers repeated signing off a cache and
all three points that have to drop it. The backend CI matrix gains the
cache configurations and now runs the Falcon API tests on every leg.
Fed in chunks, wc_PKCS7_DecodeEnvelopedData() and
wc_PKCS7_DecodeAuthEnvelopedData() failed with ASN_PARSE_E when a call
ended inside the EncryptedContentInfo header, before the end of the
content-encryption AlgorithmIdentifier.
After reading the EncryptedContentInfo SEQUENCE header, both decoders
decide whether to move to the stream buffer by comparing the bytes they
need against pkiMsgSz. When the caller's input is read directly, that is
the size of the whole input, not what is left after the current
position, so a call ending inside the header passed the check and the
parse ran past its end. When they did move to the stream buffer, the
index restarted at 0: the EnvelopedData decoder put the header offset
back only if it was already buffered, and the AuthEnvelopedData decoder
never did and kept the old pkiMsgSz, so it parsed the SEQUENCE header as
the content type. That made AuthEnvelopedData fed in fixed-size chunks
fail at most sizes from 60 bytes up.
Both now measure from where the EncryptedContentInfo starts, ask for the
header as well as the content, and skip the already-parsed header after
the move; AuthEnvelopedData also refreshes pkiMsgSz. An indefinite
length gives EnvelopedData nothing to size by, and its worst-case guess
can be more than a short message has left, so it now sizes by the
fields it reads when they are all present already.
New test_wc_PKCS7_DecodeEnvelopedData_split encodes an EnvelopedData
and an AuthEnvelopedData and decodes each in two calls, the second one
carrying each of the last 128 bytes in turn. Without the fix both halves
fail with ASN_PARSE_E. test_wc_PKCS7_DecodeEnvelopedData_fragmented also
decodes a one-byte indefinite-length DES3 message in one call, which
needs the sizing by the fields present.
wc_PKCS7_DecodeAuthEnvelopedData() failed with ASN_PARSE_E on the output
of openssl cms -encrypt -aes-256-gcm -stream. It read at most one OCTET
STRING out of a constructed encryptedContent, rejected an indefinite
length there, and expected the authenticated attributes or the tag
straight after the content, where BER puts the end-of-contents of the
content and of the EncryptedContentInfo.
The fragments are now joined into one buffer before the AEAD runs,
since GCM and CCM need the whole ciphertext to check the tag, and both
end-of-contents are consumed when present. An empty fragment or an empty
content is rejected with ASN_PARSE_E, as an empty definite-length
content already was, and the join stops with BUFFER_E as soon as the
content outgrows the output buffer.
Fed in chunks it also failed in two places. The indefinite
EncryptedContentInfo gave no length to size the read by, so too little
was buffered to parse the content type and algorithm; the worst case is
now buffered, as the EnvelopedData decoder already does. And the bytes
still expected after the tag were computed as maxLen minus the total
read, which an indefinite message has already passed, so the unsigned
result wrapped and the decoder asked for 4 GB more input. It now waits
only for what is buffered.
test_wc_PKCS7_DecodeAuthEnvelopedData_fragmented re-encodes AES-256-GCM
messages, with and without authenticated attributes, in 16, 7, 1, 33 and
4096-byte fragments, decodes them whole and in 1 and 13-byte chunks, and
checks that a flipped tag bit and an indefinite [0] with no content are
rejected and that too small an output stops the join early. test_wc_PKCS7_DecodeOpenSslStream
gains certs/test-stream-dec-aes256gcm.p7b from renewcerts.sh.
When the encryptedContent of an EnvelopedData is a constructed,
indefinite-length [0] holding several OCTET STRINGs, as
openssl cms -encrypt -stream writes it, wc_PKCS7_DecodeEnvelopedData()
decrypted each fragment into a cache sized for that fragment alone and
handed it to the stream output callback. With no callback set, every
fragment but the last was dropped, only the last was copied to the
start of the output buffer, and the return value still counted the
whole content. The caller got success and a buffer holding one block
of plaintext followed by whatever it held before.
Decrypt each fragment onto the end of the cache instead, so without a
callback the whole plaintext is there for the padding check and the
copy to output that follow the loop. With a callback, each fragment is
still passed on as it is decrypted, and the last after its padding
check; a callback that refuses a fragment ends the decode with
BUFFER_E, as it already did for the last one. Without a callback the
decode stops with BUFFER_E as soon as the plaintext can no longer fit
the output buffer, rather than joining the whole content first. When
the cache grows, the plaintext in the old buffer is wiped before it is
freed.
Four more problems in the same loop:
- Every fragment had to be a whole number of cipher blocks, which BER
does not require. Ciphertext short of a block now waits in a carry
buffer for the next fragment, and a content that does not end on a
block boundary is rejected with BUFFER_E.
- Streaming, wc_PKCS7_StreamEndCase() shifts the buffered input down
without moving the index, and the loop only re-read its position when
a stale length said it had run out. After a fragment smaller than the
data buffered, the next header was parsed from the wrong offset. The
position is now re-read at the top of every iteration.
- Whether a fragment is the last is decided by the end-of-contents that
follows it, so the refill check now also asks for those two bytes.
Without that, a chunk ending exactly after the last fragment decrypted
it as a middle one and then failed on the end-of-contents.
- A NO_PKCS7_STREAM build never set up the cipher for fragmented content
at all, so it could not decrypt any of it.
A decryption callback cannot decrypt fragmented content: it was given
the content IV for every fragment, and the wc_PKCS7_DecryptContentEx()
call that followed it ran with no key set, so that path never produced
plaintext and handed the cipher a NULL Aes, which not every port checks
for. Fragmented content with a decryptionCb set is now rejected with
BAD_FUNC_ARG before any cipher is touched.
The new state lives in PKCS7State, which is private to pkcs7.c; the
public wc_PKCS7 structure does not change.
test_wc_PKCS7_DecodeEnvelopedData_fragmented re-encodes a message the
way OpenSSL streams it, with 16, 7, 1, 33 and 4096-byte fragments, and
decodes it whole, fed in 1 and 13-byte chunks with and without a stream
callback, and split right after the last fragment. It also checks that
a callback refusing its second fragment stops the decode there, that
too small an output stops it before the whole message is read, that
stale bytes in the output buffer do not survive, that an output buffer
one byte short gets BUFFER_E, and that a content short of a block is
rejected. test_wc_PKCS7_DecodeOpenSslStream decodes
certs/test-stream-dec-aes256.p7b, made by openssl cms -stream in
renewcerts.sh. Without ASN_BER_TO_DER the fragmented messages are still
rejected with BER_INDEF_E, which the test checks.
RFC 5652 Section 6.1 derives the EnvelopedData version from every
RecipientInfo in the set: a message carrying a PasswordRecipientInfo or
an OtherRecipientInfo is version 3 whoever else it is addressed to.
wc_PKCS7_ParseToRecipientInfoSet() checked the version against the
reader's own key type instead, allowing only 0 and 2 for RSA and 0, 2
and 3 for ECC. A KTRI reader therefore could not open a message that
was also addressed to a password or ML-KEM recipient, and got
ASN_VERSION_E before the recipients were even looked at.
The version says nothing about which recipient is the reader's, so
accept any CMSVersion EnvelopedData can carry (0, 2, 3 or 4) for every
key type. Version 1 is still rejected.
The test encodes one message to a KTRI and a PWRI recipient, checks it
is version 3, and opens it as the RSA recipient, then again with the
version rewritten to 4 and to 1.
RFC 8446 Appendix D.4 describes a TLS 1.3 server answering a ClientHello
that carries a non-empty legacy_session_id with a ChangeCipherSpec. Both
server send paths sat behind WOLFSSL_TLS13_MIDDLEBOX_COMPAT, which only
--enable-tls13-middlebox-compat and --enable-jni define and which
neither enables by default, so a default-built server never sent one.
Peers
that require the record, such as Erlang's ssl, abort the handshake with
an unexpected_message alert.
Compile the two server accept-path sends unconditionally and drive them
from the runtime tls13MiddleBoxCompat flag, which the server already
clears when the client's session id is empty. This needs the
sentChangeCipher option bit compiled in for every TLS 1.3 build, and
SendChangeCipher itself, whose definition was guarded on TLS 1.2 or old
TLS or middlebox compat and so went missing from a TLS 1.3-only build
such as examples/configs/user_settings_tls13.h. That definition is now
compiled for a TLS 1.3 server, or a TLS 1.3 client in a middlebox compat
build, so a client-only build that can never call it does not carry it.
DTLS stays excluded by the existing guard. QUIC clears the flag on its
own rather than trusting the peer to honour the empty session id RFC
9001 Section 8.4 calls for, since a ChangeCipherSpec record has no way
to travel a QUIC CRYPTO stream. With ECH the outer ClientHello decides
on its own, since TLSX_ECH_Parse rebuilds the inner hello carrying the
outer session id, so no carve-out is needed for the inner re-parse.
wolfSSL_clear()
resets the flag and sentChangeCipher, or a reused server object would
answer only its first peer.
Under TLS 1.3 the record is a dummy and must not move handshake state,
so the OPENSSL_EXTRA block no longer assigns serverState for it. It ran
before any send could fail, so a retryable send left serverState reading
as ChangeCipherSpec complete and the server stopped waiting for the
second ClientHello. The info callback still fires on both sides, and the
client's clientState is untouched, so nothing an application can observe
changes. Both server sends go through one static helper,
SendTls13ServerChangeCipher, so the guard, the mark and the flush are
stated once, with the flush an argument because only the
HelloRetryRequest needs it. sentChangeCipher is marked on a WANT_WRITE
as well as on success, since SendChangeCipher appends the record to the
output buffer before the flush that stops short, and the mark is all
that stops a second one being built. That matters on the
HelloRetryRequest path, where the resumed accept does not come back to
the same case: the prologue drains the buffer and advances past
TLS13_ACCEPT_HELLO_RETRY_REQUEST_DONE, so the flight falls through into
TLS13_SERVER_HELLO_SENT, which asks for a ChangeCipherSpec of its own.
SendTls13ServerHello flushes a HelloRetryRequest as soon as it is
built, so the ChangeCipherSpec queued behind it needs a flush of its
own. Without one, a server grouping its messages, which every
OPENSSL_COMPATIBLE_DEFAULTS build does, leaves the record in the output
buffer and the peer sees a bare HelloRetryRequest.
The three client send sites in wolfSSL_connect_TLSv13 mark the same
way. The one answering a HelloRetryRequest also keeps buildingMsg
set when the send stops short, since the second ClientHello still has to
follow and the connect state machine must stay on that case while the
record drains; without it a refused write stranded the handshake.
What a client puts on the wire is unchanged. Without
WOLFSSL_TLS13_MIDDLEBOX_COMPAT it still sends an empty session id and no
ChangeCipherSpec of its own.
The test helper counts ChangeCipherSpec records rather than answering
whether there is one, so that a second record going out reads
differently from the one record that is owed.
test_tls13_middlebox_compat_hrr_ccs covers the HelloRetryRequest path
with messages grouped and ungrouped, and checks the flight answering the
second ClientHello carries no further record.
test_tls13_middlebox_compat_server_ccs_retry refuses the write carrying
the record and resumes the handshake, so a send that stops part way
still leaves exactly one on the wire, and
test_tls13_middlebox_compat_hrr_ccs_retry does the same on the retry
path, where the flight answering the second ClientHello must carry
none. test_tls13_middlebox_compat_client_ccs_retry refuses the client's
own record on both of its handshake flights, and
test_tls13_middlebox_compat_client_ccs_retry_early_data on the third
client send site, the early data one, where the record goes out ahead of
the 0-RTT records rather than as part of a handshake flight and a
resumed wolfSSL_write_early_data() would otherwise repeat it.
Those client-side tests need WOLFSSL_TLS13_MIDDLEBOX_COMPAT, which no
os-check-linux entry defined: only --enable-tls13-middlebox-compat and
--enable-jni set it, and configure.ac turns it back off when ECH is on,
as it is under --enable-all. The all-tls13-middlebox-compat entry is
--enable-all with ECH off and the option on, so they run somewhere.
Also correct the macro's doc comment, which claimed the feature defaults
to on, reword the --enable-tls13-middlebox-compat help text and the
matching CMake option description, which described the whole feature
rather than the client half they now control,
and drop a duplicated dtls test in the same condition.
wc_XmssKey_ExportPub_ex and wc_XmssKey_ExportPubRaw checked only for NULL
arguments. Parameters are populated by wc_XmssKey_SetParamStr, well before
wc_XmssKey_MakeKey, wc_XmssKey_Reload or an import produces a public key, so
a caller that had merely selected a parameter set received a zeroed public
key and a success return. The export path also marked the destination
verify-only, and wc_XmssKey_PublicKeyToDer encoded the result as a valid
RFC 9802 SubjectPublicKeyInfo.
Require the OK, VERIFYONLY or NOSIGS state in both exporters, and check it
before the destination key is zeroized so a rejected call leaves it
untouched. wc_XmssKey_GetPubLen keeps its parameter-only contract, since
sizing a buffer ahead of key generation is what callers use it for. Cover
the parameter-only case in the XMSS decision coverage test and in the
white-box export guards.
Fixes F-8151.
ForceZero() bracketed its stores with XFENCE(), which resolves to a full
CPU memory fence on every current compiler branch (mfence or lock-or on
x86, dmb on ARM64), so each wipe of per-record state paid two fences even
in single-threaded builds.
The wipe only needs to survive dead-store elimination. Cross-thread
visibility of the zeroed memory is not part of ForceZero's contract; a
caller that shares the buffer orders it with a lock or an atomic release.
Add WC_BARRIER_DATA(ptr), named after the same construct in the Linux
kernel: an empty asm statement that takes the pointer as an input operand
and clobbers memory, so the compiler must assume the buffer is read
afterwards. ForceZero() uses it before and after the wipe. The trailing
barrier keeps the zeroing stores alive. The leading one makes the
compiler materialize the buffer's live contents at mem first; without it
GCC 13 at -O3 kept a filled local in a different stack slot that the wipe
never touched. Neither barrier costs an instruction. Non-GNU compilers
and WOLFSSL_NO_ASM builds fall back to WC_BARRIER(), which keeps their
previous behavior. XFENCE() itself is unchanged for the
speculation-barrier call sites in asn.c and ssl_sess.c.
On x86-64 with GCC 13 at -O2 the zeroing stores stay in the object code
and the two lock-or fences per call are gone. The ChangeLog records the
behavioral change.
Derive is the only key store operation that creates a key without any way to
say how big it should be. Every other one has an answer: ImportPlain has the
plaintext's length, ImportWrapped reads it from the blob, the exports and
GetInfo report a property of a key that already exists. Derive has keyType,
which is a purpose rather than a length - AES is 128 or 256 either way -
derivSz, which measures the input rather than the output, and attrs, which is
three flag bits. So the device picks, the caller cannot ask, and GetInfo then
reports a size the API gave no way to request.
Add keySz, in bytes, beside the keyType it qualifies. Zero keeps the current
behaviour and leaves the choice to the device, which is the right answer for a
key store whose derivation has only one output size.
PKCS#11 carries this in C_DeriveKey's template as CKA_VALUE_LEN and PSA in the
attributes passed to psa_key_derivation_output_key(); this is the same
parameter under a name that matches wc_KeyStore_ImportPlain().
The key store API reached master without a ChangeLog entry. Add one here, since
this is the last change to that API and the entry can describe it as it ships.
The option set ENABLED_TLSV12 to no, which the summary printed and a couple of
derived settings read, but nothing defined WOLFSSL_NO_TLS12, so every line of
the TLS 1.2 implementation was still compiled and still reachable. The one
place configure.ac adds that define sits in the FIPS lean-aesgcm bundle and is
guarded by "TLS 1.2 is still on and the user did not ask for it", which is
false exactly when --disable-tlsv12 was passed. Only the CMake build honoured
the switch.
Define it once the bundles have had their say, so --enable-tinytls13 and the
other bundles that turn the version off get it as well. The case guard keeps
the FIPS bundles from adding it twice.
Add three os-check entries for the configurations this makes reachable:
--disable-tlsv12 on its own, the same with DTLS 1.3, and the same with the
sniffer. The one existing entry that passes the option,
dtls13-client-minimal, already defined WOLFSSL_NO_TLS12 through CPPFLAGS and
strips most of the crypto along with it, so it never covered the option's own
effect.
Compiling the version out reaches code that assumed it was always there.
ProcessCSR_ex parses one certificate_status message and TLS 1.3 reads the
chain's per-certificate entries through it, so it moves out of the version
guard; only its TLS 1.2 wrapper stays behind. BuildMessage's connection-ID
size is read by the TLS 1.2 record path alone, so it is declared with it. The
tests and examples that hard-code a TLS 1.2 method, or a helper that only
those call, are skipped without the version.
CMake rejected three of the four combinations configure does but let
WOLFSSL_OLD_TLS=yes through, defined NO_OLD_TLS behind the user's back and
reported the option as still on. Reject it there too, and drop the NO_OLD_TLS
the block used to add, which the old-TLS option itself now always supplies.
RFC 9846, 5.5
CheckTLS13AEADSendLimit(): at the AEAD limit while sending early data,
return TOO_MUCH_EARLY_DATA instead of calling Tls13UpdateKeys(). A KeyUpdate
there would go out pre-handshake.
RFC 9846, 6.1
Three alert sites in DoAlert() / DoProcessAlertRecord(): TLS 1.3
user_canceled is now exempt from teardown and session invalidation at any
AlertLevel, not just warning. TLS 1.2 unchanged.
RFC 9846, 4.7.3
New Tls13KeyUpdateLimitReached() helper shared by send and receive
paths. At the 2^48-1 cap, DoTls13KeyUpdate() drops a peer's update_requested
and continues, rather than failing the connection. App-initiated
wolfSSL_update_keys() still returns BAD_STATE_E.
RFC 9846, 4.3
TranslateErrorToAlert() maps BUFFER_E to decode_error as well as
BUFFER_ERROR — one case label covering 27 malformed-extension sites that
previously aborted with no alert sent.
Tests added.
With WOLFSSL_SNIFFER defined, the example client and server pin themselves to
a static RSA and static ECC cipher list so a capture can be decrypted. The
guard for that read "version < 4", meant to leave TLS 1.3 alone, but the
examples encode a DTLS version as a negative number and DTLS 1.3 is -4, so a
"-u -v 4" run was handed a TLS 1.2 only cipher list.
The server then found no common TLS 1.3 suite in MatchSuite_ex() while
processing the ClientHello and failed with MATCH_SUITE_ERROR, which reached
the client as a missing_extension alert. Every DTLS 1.3 handshake between the
two examples failed in a sniffer build, which also took out the DTLS cases of
scripts/ocsp-stapling_tls13multi.test. Only the examples were affected; an
application that does not set that cipher list was always able to handshake.
The sniffer's session, server and secret tables are qualified THREAD_LS_T, so
each thread that calls ssl_InitSniffer*() owns its own. Its trace file, its
mutexes and its crypto device are not: they are shared by the whole process.
ssl_FreeSniffer() released all of it on every call, so in a build such as
snifftest with THREADED_SNIFFTEST, where every worker thread initializes and
frees the sniffer for itself, a thread that finished early closed the trace
file and freed the mutexes under the threads still running. A worker that was
still registering its key then crashed inside ssl_SetNamedEphemeralKey(),
writing to a file another thread had closed:
flockfile -> vfprintf -> fprintf -> ssl_SetNamedEphemeralKey -> load_key
Count the callers of ssl_InitSniffer_ex(), which ssl_InitSniffer() and
ssl_InitSniffer_ex2() both funnel through, and only release the shared state
once the count reaches zero. The count is atomic where the platform offers
atomics and a plain int otherwise, which covers the single threaded build. The
per-thread tables are still freed on every call, and wolfSSL_Cleanup() is
still called every time, since it keeps a count of its own.
Verified with two ssl_InitSniffer() callers and one ssl_FreeSniffer(): before
this the trace file was closed by the early free and everything traced after
it was lost, now both entries are present. The full sniffer test suite passes
with and without THREADED_SNIFFTEST.
snifferWorker() looped while the shutdown flag was clear and only then
drained its queue. Reading a capture file, main() enqueues every packet
and then sets that flag and joins, normally all before the worker thread
is scheduled for the first time, so the worker woke up, saw the flag and
returned without decoding a single packet. Instrumenting the loop shows
it exiting with zero iterations and a non-empty queue, at -O0 as well as
-O1, so this is an ordering race rather than a hoisted load.
Keep going after shutdown for as long as packets are queued. The check
has to be its own helper that takes the worker's semaphore, because a
placeholder head means an empty queue: looping on worker->head alone
would spin forever on a placeholder nothing will ever fill. shutdown and
unused are polled across threads, so mark them volatile.
That alone left -keylogfile broken in this build. The sniffer's server
and secret tables are thread local, so every worker has to repeat the
setup main() did for itself, but the workers only ever called load_key()
and so reported every packet as coming from an unregistered server. Carry
the keylog path in SnifferWorker and load the secrets and create the
keylog sniffer server per worker.
Verified with --enable-sniffer CFLAGS=-DTHREADED_SNIFFTEST: the whole
sniffer test suite passes, and the multi-session captures decode the same
application data at 1, 2, 4 and 8 threads as they do without threading.