Under WOLF_C89, XSNPRINTF is sprintf and ignores the size argument.
WOLFSSL_ERROR_LINE formatted the source file name into an 80-byte
buffer and wolfSSL_ERR_print_errors formatted the reason and file into
a 160-byte one, so long __FILE__ paths, as CMake builds produce,
overflowed the stack. GetTimeString wrote its 24-character string
regardless of the length wolfSSL_ASN1_TIME_to_string was given.
Bound the file name and reason with a precision, and format the time
into a local buffer and copy it only when it fits the caller's length.
DecodePolicyOID and GenerateDNSEntryRIDString wrote each OID arc
directly into the output with XSNPRINTF and rejected the result only
after the fact. Under WOLF_C89, XSNPRINTF is sprintf, so a certificate
with a long certificatePolicies OID or registeredID alt name wrote past
the output buffer while the certificate was parsed, before any
application call. DecodePolicyOID also serves wolfSSL_OBJ_obj2txt, where
it overflowed the caller's buffer.
Format each arc into a small local buffer and copy it only when it fits.
Behaviour with a bounded snprintf is unchanged.
wolfssl_print_number wrote each "xx:" group straight into the 50-byte
line buffer and checked the returned length to decide when to wrap.
Under WOLF_C89, XSNPRINTF is sprintf, so the group that did not fit was
written past the end of the line before the check, overflowing the
stack on every RSA, DH or DSA key print with more than 15 bytes.
Format each group into a small buffer and copy it only when it fits.
X509CRLPrintExtensions converted the CRL number to decimal in a 49-byte
stack buffer. A 20-octet CRL number, the maximum the parser accepts by
default, can need 49 digits, so the terminating NUL was written one
byte past the buffer when printing such a CRL with
wolfSSL_X509_CRL_print. Builds that raise CRL_MAX_NUM_SZ accept longer
numbers and overflowed it further, and the decimal string was then
formatted into an 80-byte line, which under WOLF_C89 is an unbounded
sprintf.
Size the buffer from CRL_MAX_NUM_SZ and write the number to the BIO
directly.
Fixes F-9101.
Several X509 print helpers formatted certificate-controlled data into
fixed stack buffers through XSNPRINTF and relied on checking the
returned length afterwards. Under WOLF_C89, XSNPRINTF is sprintf, so
the write overflowed before the check ran:
wolfSSL_X509V3_EXT_print wrote a key identifier as hex into a 129-byte
buffer, X509PrintSerial_ex wrote serials of up to 32 bytes into an
80-byte line, and X509PrintExtensions wrote the dotted OID of an
unrecognised extension into an 80-byte line.
Bound the key identifier with a precision, format each serial byte into
a small buffer and flush the line when full, and write extension OIDs
to the BIO directly. In builds with a bounded snprintf, long serials no
longer fail the print.
DhSetKey() only tests the server's DH prime when dhDoKeyTest is set and
dhKeyTested is clear. On the client both flags latched after the first
handshake: a successful test set dhKeyTested, and an FFDHE match in
GetDhPublicKey() cleared dhDoKeyTest. Neither wolfSSL_clear() nor
ReinitSSL() re-armed them, so a second handshake on a reused object, or a
renegotiation, installed the new server's p and g as trusted and accepted
a composite prime.
GetDhPublicKey() now clears dhKeyTested whenever it installs parameters
from the wire, and marks an FFDHE match as tested instead of clearing
dhDoKeyTest. dhDoKeyTest is left as the policy set by
wolfSSL_SetEnableDhKeyTest().
SendServerKeyExchange() allocates serverDH_Priv only when the buffer is
NULL, sized for the current prime, and DhGenKeyPair() then overwrites its
length with the size of the generated key. With secure renegotiation
enabled, FreeHandshakeResources() keeps the buffer, and wolfSSL_clear()
does not free it either. A renegotiation, or a reused server object, that
switches to a larger prime then hands GeneratePrivateDh() a buffer sized
for the previous key and the handshake fails with WC_KEY_SIZE_E.
Both DHE branches now zeroize and free a retained buffer that is shorter
than the new prime needs, so it is allocated again at the right size. The
public key buffer already had a renegotiation-only realloc for the same
reason.
The SSL_SESSION_set1_id and set1_id_context regression test needs
OPENSSL_ALL as well as the client cache reference path. With
NO_SESSION_CACHE_REF now the configure default, the plain
--enable-opensslall legs no longer take the reference path. The
session-cache-ref leg is the only one left on it, and it built with
opensslextra only, so the test skipped everywhere. Adding
--enable-opensslall keeps it running.
On the client side wolfSSL_get_session() returns a ClientSession locator
into the static ClientCache rather than a full WOLFSSL_SESSION, unless
NO_SESSION_CACHE_REF is defined. Every session accessor maps that locator
to the real cache entry through ClientSessionToSession(), but
wolfSSL_SESSION_set1_id() and wolfSSL_SESSION_set1_id_context() wrote
the session ID and context straight through it. The writes landed at
WOLFSSL_SESSION offsets well past the locator, corrupting neighbouring
ClientCache entries and, for the last entries, running past the array.
Resolve the pointer first in both functions and fail when it no longer
maps to a cache entry. A cache entry is keyed by its session ID, so
rewriting it in place would stop the handle from resolving and strand
the entry in the row of its old ID. set1_id() on a client handle
therefore only accepts the ID the entry already holds, and then writes
nothing. Callers that want a new ID can use wolfSSL_get1_session(),
which returns the connection's own session.
Add a regression test that drives the documented get_session,
set1_id_context and set1_id flow on a client handle: the context reaches
the cached session, re-keying is refused and the handle keeps resolving,
an owned session still takes a new ID, and both setters reject a handle
whose entry was evicted.
Fixes F-9049.
Session cache entries carry no heap hint and EvictSessionFromCache frees
the ticket nonce with NULL, but AddSessionToCache allocated it from the
adding session's heap. With static memory a pool block was handed to
free() on eviction. Use NULL for every alloc and free of the cached
nonce, and free ticBuff with the NULL heap it was allocated from.
Thanks to ByteRay Ltd. for the report.
Fixes F-12725, F-11822.
FreeSSL_StaticMemory() frees the ssl->heap hint block, but the DTLS 1.3
FSM, QUIC and dual-algorithm peerSigSpec cleanups that followed it still
free through ssl->heap, reading the pool pointer out of a block already
returned to the pool. Move it to the end.
wolfSSL_set_SSL_CTX() replaces ssl->ctx but keeps ssl->heap, yet
wolfSSL_free() and FreeSSL_StaticMemory() freed the WOLFSSL object, its
heap hint and its stats block with the current ssl->ctx->heap. After an
SNI callback swapped to a CTX with a different heap, pool blocks went to
libc free() (NULL heap), onto another pool's free lists (second pool),
or a malloc()ed WOLFSSL was treated as a pool block (NULL to pool).
Pass ssl->heap from wolfSSL_free() and free through a local hint that
carries only the pool ssl->heap draws from. FreeSSL() cannot use
ssl->heap itself because wolfSSL_ResourceFree() frees that hint block
before the final XFREE of the WOLFSSL object.
Thanks to ByteRay Ltd. for the report.
Go back to the one-byte sigSz probe for SignWithRandom() with a
(NULL, 0) message in test_wc_SlhdsaDecisionCoverage(), so the test
stays argument-check only. The signing path for (NULL, 0) is covered
in test.c.
Update the Japanese doxygen for SignDeterministic, SignWithRandom,
Sign and Verify to match the English header.
- Source comments: msg may be NULL if msgSz is 0 for the sign and
verify functions, and Sign(), Verify() and slhdsakey_sign_external()
reject a NULL msg only when msgSz is greater than 0.
- Doxygen: note on the msg parameter, and SignDeterministic() returns
BAD_LENGTH_E for a too-small signature buffer.
- test.c: SignDeterministic() with (NULL, 0) must match the signature
for a non-NULL message of length 0.
- test_slhdsa.c: SignWithRandom() with (NULL, 0) and a full-size
signature buffer signs successfully.
wc_SlhDsaKey_SignWithRandom() rejected msg == NULL for any length, so it
and wc_SlhDsaKey_SignDeterministic() returned BAD_FUNC_ARG for an empty
message passed as (NULL, 0), unlike wc_SlhDsaKey_Sign() and Verify().
Use the same check as Sign() and, like Sign(), replace a NULL message
with a one-byte static stand-in before the crypto callback dispatch.
Update the doxygen and source comments, and add tests for the (NULL, 0)
case.
tests/swdev/Makefile localized every swdev symbol except the two entry
points with objcopy --keep-global-symbol, which macOS does not ship. On
Darwin, do the same with a second ld64 relocatable link and
-exported_symbol. The export check strips the Mach-O leading underscore
before comparing. The exported names now live in one SWDEV_EXPORTS list
that feeds both localization commands and the check.
The localization step is picked from configure's host_os rather than
uname, so cross-builds use the target's toolchain.
Add a macOS CI config that builds swdev with WOLF_CRYPTO_CB_ONLY_ED25519,
so make check depends on the ld64-localized swdev object.
The AES implementation chain at the top of aes.c picks one block cipher
backend, and the hardware ports ahead of WOLFSSL_ARMASM in it (STM32,
LTC, ...) store a raw key in aes->key. ECB, the software CTR mode, GCM
(subkey setup and the bulk encrypt/decrypt) and XTS tested
WOLFSSL_ARMASM directly, so with such a port and WOLFSSL_ARMASM both set
they ran the Arm assembly on the raw key and returned wrong output. On
STM32 this hit ECB and CTR (STM32 selects NEED_AES_CTR_SOFT), and GCM
wherever it falls back to software: WOLFSSL_STM32_BARE with a
non-12-byte IV, or parts without STM32_CRYPTO_AES_GCM. e31e158225 had
excluded STM32_CRYPTO for the GCM subkey only.
Define WC_AES_USE_ARMASM in the WOLFSSL_ARMASM branch of the chain and
test it in those places instead of WOLFSSL_ARMASM. The ports keep their
own block functions there, so ECB goes through wc_AesEncrypt() and
wc_AesDecrypt(), CTR through the port's block transform, GCM through
AES_GCM_encrypt_C() and AES_GCM_decrypt_C() (with IncrementGcmCounter()
now built for them on AArch64) and XTS through its software
implementation. The GHASH-only selectors keep WOLFSSL_ARMASM since they
only use H, and the AArch64 streaming GCM branches also require
use_aes_hw_crypto, which only the Arm assembly SetKey sets. This
replaces the STM32_CRYPTO exclusion from e31e158225. Builds that select
the Arm assembly backend are unchanged.
Tested on an STM32U585 (wolfDemo) with STM32 AES and HASH plus Thumb-2
assembly: the wolfCrypt test passes, including AES, AES-CTR, AES-GCM,
AES-CCM and CMAC. With STM32 AES, aes.o no longer references
AES_ECB_encrypt(), AES_ECB_decrypt() or AES_CTR_encrypt(). An AArch64
--enable-all --enable-armasm build passes the wolfCrypt test and unit
tests, and the same build with WC_AES_USE_ARMASM left undefined (as with
a port ahead of the Arm backend) now compiles and passes all AES tests,
including XTS and the streaming modes. Thumb-2 and AArch32
crypto-extension objects are unchanged.
The verify-only SLH-DSA benchmark only had a SHAKE-128s vector, because
that is the only verify-only signature in wolfcrypt/test/test.c. Builds
with only the SHA2 parameter sets showed no SLH-DSA row at all.
Add a SHA2-128s vector: a deterministic, empty-context signature of
"Hello World!" by the existing bench_slhdsa_sha2_128s_key.der. It was
generated with OpenSSL 4.0.3; wolfSSL verifies it, rejects it once a
message bit is flipped, and its own deterministic signature from the
same key is byte-identical. gen-slhdsa-mldsa-certs.sh gains the step
that produces it, and a rerun reproduces the committed files. The pq-all
verify-only benchmark config enables sha2 and requires this row too.
bench_slhdsa_verify() now takes the parameter set and vector. The row
uses the full benchmark's name, SLH-DSA-SHA2-S, and takes its strength
from the public key size, as the full benchmark does. Verify-only
builds accept -slhdsa-sha2-128s when SHA2-128s is built.
Tested on the host with --enable-slhdsa=yes,sha2,verify-only on its
own, with --enable-smallstack and with --disable-rng, with sha2 only,
with SHAKE only and 192f only (no SHA2 row, no warnings), and with the
full sha2 build.
The benchmark measured LMS, XMSS and SLH-DSA verify only after
generating a key and signing with it, so a build with
WOLFSSL_LMS_VERIFY_ONLY, WOLFSSL_XMSS_VERIFY_ONLY or
WOLFSSL_SLHDSA_VERIFY_ONLY compiled the algorithm out of the benchmark
entirely. Targets that only check signatures, such as a boot loader
verifying an image, got no numbers at all.
Verify-only builds now time verify against a fixed vector: LMS
L1_H10_W8, XMSS-SHA2_10_256 and SLH-DSA-SHAKE-128s. The vectors are the
verify-only KATs already in wolfcrypt/test/test.c, stored as binary
files under certs/lms, certs/xmss and certs/slhdsa and emitted into
certs_test.h by gencertbuf.pl. The rows use the same names as the full
benchmark, and a parameter set without a vector, or one that is not
compiled in, is skipped. If loading the key fails, the failing call is
printed and no row is shown.
In a verify-only build, -lms_hss, -xmss_xmssmt(_sha256),
-slhdsa-shake128s and -slhdsa are accepted only when their vector
applies, and the other XMSS and SLH-DSA parameter options are rejected,
so no option silently runs nothing.
Verify needs no RNG, so the asymmetric result printing is built
whenever one of these vectors applies, also with WC_NO_RNG. A
verify-only build with no vector no longer counts as an asymmetric
algorithm. This also fixes an LMS verify-only build with no other
asymmetric algorithm, which failed -Werror on master with an unused
bench_stats_asym_finish(). The SRTP-KDF, scrypt and Argon2 benchmarks
also call it, so it is now built for them too: on master each of them
failed to compile with no asymmetric algorithm, which an LMS
verify-only build used to hide.
The benchmark exits 0 even when verify fails, so a new pq-all CI config
builds all three verify-only with no other asymmetric algorithm and
-Werror=unused-function, runs the benchmark, and fails on any reported
benchmark error, such as a key that does not import, or on a missing
verify row.
Tested on the host with --enable-{lms,xmss,slhdsa}=yes,verify-only on
its own, with --enable-smallstack, with --disable-rng, and with
--enable-all-crypto; with edge configurations where no vector applies
(SLH-DSA 192f only, LMS max height 5, XMSS min hash size 512), both
with --disable-rng and with RNG but no other asymmetric algorithm; with
SRTP-KDF, scrypt or Argon2 as the only such user, with and without an
RNG and with LMS verify-only; and
with --enable-all and the full LMS/XMSS/SLH-DSA build, whose benchmark
output is unchanged. The new CI config passes under
parallel-make-check.py and fails with a corrupted signature byte, a
truncated public key, a missing row and an unused static function.
scripts/falcon_fpr_ct_check.py compiles the integer fpr emulation with
gcc and clang for Cortex-M0+, M3, M7 and M33, RV32 and RV64, i386,
x86_64, AArch64, ESP32 and ESP32-S3 at -Os and -O2, and reports any
conditional branch outside the fixed-count loops of fpr_div and
fpr_sqrt and any compiler runtime call other than the 64-bit multiply.
A second pass compiles all of falcon.c with -g for the default and
smallest memory signers at -Os, -O2 and -O3, and reports any branch the
line table attributes to the fpr block: inlining into the FFT, sampler
and key generation code can undo what the block shows on its own. Stub
libc headers stand in for a sysroot.
It is meant for local runs after changes to the fpr code. A missing
toolchain is an error unless --allow-missing is given, and the cross
compilers can be named with ARM_GCC, RV_GCC, A64_GCC, XT_GCC and
XT3_GCC. Against master it fails with clang on Cortex-M0, RISC-V and
i386 and with gcc on ESP32, and the second pass fails on x86_64 when
the barriers are dropped there.
gcc for Xtensa computes the carry of a 64-bit add and the borrow of a
64-bit subtract with a conditional branch. Every Espressif gcc from 13.2
to 16.2 does so on ESP32 (LX6) at any optimisation level and on
ESP32-S2/S3 at -Os; only gcc 14 and later at -O2 on LX7 use saltu
instead. The fpr emulation and the key generation big-integer code
accumulate secret values in 64-bit integers, so on ESP32 they branched on
secret data: about 106 branches in the fpr operations, 76 of them in the
multiply-high helper of fpr_expm_p63, plus zint_bezout,
zint_co_reduce(_mod), zint_mul_small, zint_add_mul_small,
poly_sub_scaled, modp_montymul and the key generation Gaussian sampler.
On Xtensa, falcon_add64() and falcon_sub64() split the operation into
32-bit halves and derive the carry from the top bits, and
falcon_mask64() and falcon_nmask64() build 0/all-ones masks in 32 bits
and sign-extend them. Elsewhere they expand to the previous expressions:
clang output for Cortex-M0, M7 and M33, ARMv7-A, i386, x86_64 and
AArch64 is byte-identical, and gcc on AArch64 only reschedules
poly_sub_scaled.
With Espressif gcc 13.2, 14.4, 15.x and 16.x the fpr operations are now
branch-free on ESP32 and ESP32-S3 at -Os and -O2, and the branches left
in falcon.c are loops and checks on public values. Results are
unchanged: with the Xtensa helpers forced on the host, the fpr
operations agree with the previous code on 20M inputs per operation, and
the deterministic vector and the Falcon tests pass.
The fpr shift helpers have a separate implementation for targets
without WC_64BIT_CPU, which no CI job built. Add a smallest-mem leg with
-DWC_32BIT_CPU so the white-box exercises it.
The fpr emulation still turned most of its 0/1 masks into conditional
branches on targets without a conditional select. With clang -O2 for
Cortex-M0 the emulation compiled to 122 conditional branches; only the
fpr_floor, fpr_mul and fpr_add swap selects had a barrier. Put
fpr_ct_opaque32() on the remaining selectors in FPR(), FPR_NORM64,
fpr_scaled, fpr_rint, fpr_trunc, fpr_add, fpr_half, fpr_div, fpr_sqrt
and fpr_lt. Only the fixed-count fpr_div and fpr_sqrt loops still
branch.
For the range clamps the barrier goes on the value before its sign bit
is taken; otherwise clang still derives the bit with cmp/bhi. FPR_NORM64
puts a barrier on the word it tests as well as on the bit, because
clang -O3 for Cortex-M0 otherwise derives whether the word is nonzero
with cmp/bhs. The fpr_add swap condition is now the sign of
za - sign(x), which is the same predicate with a single sign
extraction.
fpr_ct_opaque32() is a no-op for gcc and clang on AArch64, where both
compilers keep every fpr select branch-free without barriers at -Os,
-O2 and -O3, also after inlining into the FFT and the sampler. That
makes arm64 signing about 5% faster than with the #11639 barriers.
Every other target keeps them: x86_64 has cmov, but once the fpr
operations are inlined clang turns the FPR() underflow clamp and the
fpr_add and fpr_half masks into branches, and 64-bit RISC-V has no
conditional select without Zicond.
The results are unchanged: the old and new fpr operations agree on 20M
random and edge-case inputs per operation, with both shift
implementations, and the deterministic vector passes.
falcon_native_make_key allocated f, g, F and G (4n bytes) next to the key
generator's scratch, although only f, g and F are encoded and G is
recomputed whenever it is needed. falcon_keygen now accepts G == NULL;
solve_NTRU already places G in its own scratch in that case, after the
last use of that area. The arena shrinks to 3n bytes.
The peak heap of key generation drops from 16,960 to 16,448 B
(Falcon-512) and from 33,344 to 32,320 B (Falcon-1024). Keys are
unchanged: the deterministic vector and 2,000 seeded signatures per level
are byte-identical.
The white-box guard test no longer expects BAD_FUNC_ARG for G == NULL.
The sampler PRNG buffered eight SHAKE256 blocks (1,088 bytes) in
falcon_sampler_ctx, which every signer allocates next to its workspace.
It now holds one block and counts its position within a batch of eight.
An 8-byte read that crosses a block boundary inside the batch is put
together from both blocks; at the end of a batch the leftover bytes are
dropped, as before. The consumed stream is therefore unchanged: the
deterministic vector and 2,000 seeded signatures per level are
byte-identical to the previous code with the smallest memory, tree,
emulated and native double signers.
The peak heap of a signature drops by 952 bytes at both levels; for the
smallest memory signer, 12,184 -> 11,232 B (Falcon-512) and 22,424 ->
21,472 B (Falcon-1024). The number of Keccak permutations is unchanged
and signing speed is the same.
Built with -Os, as Zephyr and most embedded projects do by default, GCC
ignores the inline hint on falcon_barrett, falcon_csub, the mod-p helpers
and falcon_mulhi. Every NTT butterfly then made two or three calls and
spilled its loop state to the stack, about 45-60 cycles per butterfly on
a Cortex-M7.
FALCON_HOT_INLINE adds always_inline to those helpers under GCC and
clang. On a STM32H743 at 480 MHz with -Os:
- smallest memory Falcon-512 sign: 18.9 -> 14.8 ms
- default signer Falcon-512 sign: 11.7 -> 10.8 ms
- Falcon-512 verify: 0.84 -> 0.65 ms
-O2 builds already inlined them and are unchanged.
Each attempt of the smallest memory signer recomputed several values it
had already produced, because the 20n byte workspace left no room to keep
them. Per Falcon-512 attempt it ran 34 forward and 13 inverse NTTs, five
hash-to-point squeezes and 27 key decodes, against 23 forward NTTs in
Pornin's reference.
- a = ff* + gg* was computed before the first sampling and again after
it. Every coefficient satisfies |a[i]| <= a[0] = ||(f,g)||^2, which a
conformant key bounds by 16822, so the n/2 coefficients needed fit as
int16 in the idle half of w0. G moves to the other half. A basis with
a[0] above 32767 is refused with BAD_FUNC_ARG, and falcon_privkey_decode
now refuses keys at or above the keygen bound, so every signer rejects
the same keys.
- The targets c*f and c*F were derived again at the end only to fold r/q
into the sampled z. The sampler leaves now write z - r/q mod p directly:
each leaf reads r from the slot it overwrites.
- The first Gram value reuses f in the q NTT that the first target left
behind.
- Rebuilding G already transforms g, F and f mod q, so it now also forms
the q half of Ff* + Gg*, and falcon_sm_gram runs only the p half.
An attempt now runs 22 forward and 9 inverse NTTs, three squeezes and 16
decodes. The workspace stays at 20n bytes. Every change is exact integer
arithmetic: 2,000 seeded signatures per level are byte-identical to the
previous code with the emulated and the native double backends, and with
cache-basis.
Falcon-512 signing, medians:
- arm64 native double: 0.288 -> 0.224 ms (L1), 0.596 -> 0.453 ms (L5)
- arm64 emulated fpr: 1.346 -> 1.267 ms (L1), 2.928 -> 2.723 ms (L5)
- STM32H743 double FPU: 22.5 -> 18.9 ms
The white-box test covers the fused Gram output, the a[0] bound and the
decoder's norm check.
falcon_native_verify_msg placed its 2*n word16 working buffer on the stack
unless WOLFSSL_SMALL_STACK was set. That buffer is sized for the highest
enabled level, so a Falcon-512 verify used 4 KB of stack for it whenever
Falcon-1024 was built in, and verify reached about 5.7 KB of stack on a
Cortex-M33/M7 while keygen and sign stay near 1.2-1.4 KB.
Verify now allocates the buffer per call, sized for the key's own level
(2 KB for Falcon-512, 4 KB for Falcon-1024), so it can return MEMORY_E;
the wc_falcon_verify_msg documentation says so. WOLFSSL_NO_MALLOC builds
keep the buffer on the stack. To keep it off the stack without an
allocator, WOLFSSL_FALCON_VERIFY_NO_MALLOC
(--enable-falcon=verify-no-malloc) holds the buffer in falcon_key
instead, sized for the highest enabled level, the same approach as
WOLFSSL_MLDSA_VERIFY_NO_MALLOC. With it, a key verifies one signature at
a time and falcon_key grows by 4*FALCON_MAX_N bytes.
On an STM32H743 a Falcon-512 verify goes from 5648 B of stack and no heap
to 1568 B of stack and 2048 B of heap, at the same speed. The peak stack
of a TLS 1.3 handshake that verifies a Falcon-512 chain drops from 6.3 KB
to 3.7 KB.
The falcon-interop backend matrix gains verify-no-malloc and
level1+dynamic-keys+verify-no-malloc legs running the full unit suite.