wc_LmsKey_Verify and wc_XmssKey_Verify accepted only a key in the OK or
VERIFYONLY state before calling the device. A key from InitId or
InitLabel with its parameters set is in PARMSET, and only Reload moves
it on. A verify-only build compiles Reload out, so such a key could
never verify, on any device.
Accept PARMSET before the device call for a key named by id, label or
device context. wolfHSM names a key cached on the HSM only by devCtx,
with no id. Any other key in PARMSET has no public key anywhere and
still gets BAD_STATE_E without reaching the device. Software still
refuses an id key after the device declines, because a key in PARMSET
never has a public key and the existing pubSet check returns
BAD_STATE_E.
test_wc_LmsKey_id_verify and test_wc_XmssKey_id_verify verify with such
a key through a device, by id and by device context. They check that a
key with neither gets BAD_STATE_E without the device being called, and
that the id key fails without a device: BAD_STATE_E, or NO_VALID_DEVID
in a callback-only build with no software verify. Both run in
verify-only builds, so the guard of the verify callback they share now
covers those builds.
wc_InitRsaKey_Id and wc_InitRsaKey_Label name a key held by a crypto
callback device, so the modulus n stays zero in software. Padding read
the key size from mp_count_bits(&key->n) and so got 0 bits:
- WC_RSA_NO_PAD encrypt returned RSA_PAD_E before the device was called.
- WC_RSA_NO_PAD decrypt and verify returned RSA_PAD_E after the device
had done the operation.
- PSS with a 1024-bit key and SHA-512 picked the wrong default salt
length, so signing and wc_RsaPSS_VerifyCheck returned PSS_SALTLEN_E.
Add RsaModulusBits, which falls back to wc_RsaEncryptSize when n is not
set. That already asks the device with WC_PK_TYPE_RSA_GET_SIZE, and
covers other keys held in hardware the same way. Use it wherever
padding, unpadding and the PSS salt length need the key size.
test_wc_RsaId_CryptoCb runs these operations with a key from
wc_InitRsaKey_Id against a device that holds the software key. The PSS
half needs only PSS, SHA-512 and the 1024-bit client key, read from
certs/1024 or taken from certs_test.h with USE_CERT_BUFFERS_1024, so it
also runs in builds without WC_RSA_NO_PADDING, such as the cryptocb-only
CI legs. The device does the raw operation in software, so the test
needs software RSA, or swdev in a build with WOLF_CRYPTO_CB_ONLY_RSA.
wc_HmacInit_Id and wc_HmacInit_Label name a key held by a crypto
callback device, and the caller keys the Hmac with wc_HmacSetKey(NULL,
0). Two things lost that key.
Keying the Hmac again, for a new MAC or another hash, frees it first,
and wc_HmacFree zeroes the whole struct. Only devId was put back, so the
id and label were gone and the device got a key with no name and no
bytes. Restore the id and label as well, as devId is.
When the device declined an update or final, the software path ran
with the empty key from wc_HmacSetKey(NULL, 0) and returned a MAC under
it. Return MISSING_KEY instead when the Hmac is named by id or label and
has no key bytes, as AES does for a key held on a device. An Hmac named
by id that was given key bytes still uses them. A build with PK
callbacks but no crypto callbacks also has wc_HmacInit_Id, but no device
to hold the key and no keyRaw to tell, so the check is left out there.
The check uses keyRaw, which is NULL only after wc_HmacSetKey(NULL, 0).
keyLen keeps only the low 16 bits of the length, so it reads 0 for a
key of 64 KiB.
PKCS#11 declined every such key on a token that reports a minimum HMAC
key size, because it checked hmac->keyLen, which is 0 when the key is
on the token. Check the length only for a key passed in from software;
C_SignInit checks the token key.
test_wc_HmacId_CryptoCb keys an Hmac from wc_HmacInit_Id twice and
checks that the device still sees the id, then that a declined update
and final return MISSING_KEY. It also checks that a label survives a
second wc_HmacSetKey. test_wc_HmacId_NoDevice checks that an Hmac named
by id MACs with key bytes it is given, in builds with or without crypto
callbacks, also with a 64 KiB key, and that one without key bytes gets
MISSING_KEY when no device takes it.
When the device declines a SigsLeft query, wc_LmsKey_SigsLeft and
wc_XmssKey_SigsLeft fall back to software. That is right for a key whose
state software holds, such as one made or reloaded through a devId that
only routes other algorithms. But a key whose state lives on the device,
named by id or reloaded without a read callback, has none in software.
XMSS then called the unset read_private_key and crashed. LMS counted
from the all-zero priv_raw and reported a signature left.
Fall back to software only when it holds the private key: privSet for
LMS, which only a software MakeKey or Reload sets, and sk plus the read
callback for XMSS. Otherwise report no signatures left, as before
d56831c90d. The doc comments list this as a reason for returning 0.
wc_LmsKey_Sign and wc_XmssKey_Sign had the same fallback. When the
device declined to sign with such a key, software signed with the unset
priv_data or sk and crashed. A key generated on the device has no sk
either, so XMSS crashed there even with read and write callbacks set.
Return BAD_STATE_E when the LMS privSet or the XMSS sk is not set.
wc_LmsKey_MakeKey and wc_LmsKey_Reload left priv_raw and priv_data in
place when generating or reading the key, or a later step such as
writing out a new key, failed, so a key that was never saved or loaded
still counted from priv_raw. On any failure after the state and callback
checks, wipe priv_data, priv_raw and the expanded key state and clear
privSet, which is now set as soon as priv_raw holds the key. The
priv_data buffer is kept rather than freed, so a caller that supplied
its own, as WOLFSSL_NO_MALLOC builds must, can retry. A call refused by
those checks leaves a loaded key alone.
test_wc_LmsKey_id_sigsleft and test_wc_XmssKey_id_sigsleft query a key
from InitId on a device that declines, and the LMS test also signs with
it. test_wc_XmssKey_keygen_sign_declined signs with a key generated on
a device that then declines. test_wc_LmsKey_reload_fail_sigsleft and
test_wc_LmsKey_makekey_fail_sigsleft query a key whose Reload failed to
read or whose MakeKey failed to write, and check that a priv_data buffer
the caller supplied is still in place. The reload_devid tests now also
check that a software-held key on a declining devId still reports
signatures left.
wc_MlKemKey_Init_Id and wc_MlKemKey_Init_Label name a key held by a
crypto callback device. They set no key flags, because the key material
stays on the device. But wc_MlKemKey_Encapsulate returned BAD_STATE_E
for a key without MLKEM_FLAG_PUB_SET before calling the device, so a key
named by id could never encapsulate, on any device. A PKCS#11 token key
reloaded by CKA_ID never reached Pkcs11MlKemEncapsulate, which would
find the token public object by id or label.
Skip that early check for a key named by id, label or device context,
and check MLKEM_FLAG_PUB_SET again after the device call, as
wc_MlKemKey_Decapsulate does for MLKEM_FLAG_PRIV_SET since 55084c9a50.
The device context counts because wolfHSM names a key cached on the HSM
only by devCtx, with no id. A device can then encapsulate with a key it
holds. When it declines, or there is no device, a key without a public
key still gets BAD_STATE_E before software draws randomness or reads
any key material. wc_MlKemKey_EncapsulateWithRandom does not call the
device, so its own check stays where it is.
Any other key without a public key must still fail before the device is
called. With no id, label or device context, Pkcs11MlKemEncapsulate
falls back to the first ML-KEM public key on the token, so a caller that
never loaded the peer's key would get a shared secret for the wrong
recipient. Decapsulation is left as 55084c9a50 made it: a key bound to
a device only by its devId still reaches the device, where the token's
own private key is the one the caller means.
test_wc_mlkem_encapsulate_id_key encapsulates with a key from
wc_MlKemKey_Init_Id through a callback device. It checks that the
device is called, that BAD_STATE_E comes back when the device declines,
that a key from wc_MlKemKey_Init without key material never reaches the
device, and that the same key does once it has a device context.
test_wc_mlkem_decapsulate_id_key now also checks that a key bound to
the device only by its devId reaches it.
When a SETKEY device claimed a new key through wc_AesSetKey(),
wc_AesCcmSetKey() or wc_AesSetKeyDirect(), the set-key wiped the old
key schedule and devKey but left gcm.H and the GHASH table M0 of an
earlier software GCM key in the context. Only wc_AesGcmSetKey() wiped
them. gcmKeySet is already cleared on that path, so the stale H could
not be used, but it is key material derived from the old key and goes
with the rest of it.
The pre-claim wipe in the generic, ARM32 and PPC set-key now clears H
and M0 too, through a WC_AES_GCM_WIPE_H() macro that wc_AesGcmSetKey()
uses as well.
The CryptoCb AES set-key tests in test_aes.c took buffer sizes with
sizeof, which is the size of a pointer under WOLFSSL_SMALL_STACK. In
such builds test_wc_CryptoCb_AesSetKey and
test_wc_CryptoCb_AesGcm_EncryptDecrypt failed at their first set-key
with an 8-byte key, and the declined re-key check passed an 8-byte tag
that wc_AesGcmEncrypt() rejects before it asks the device. Both tests
now use the allocation sizes.
When a WOLF_CRYPTO_CB_SETKEY or WOLF_CRYPTO_CB_AES_SETKEY callback
claims an AES key, wc_AesSetKey() builds no software key schedule. The
wrapper still marked the context keyInstalled because the set-key
returned 0, so a mode the device then declined passed the
WC_AES_KEY_IS_SET guard and ran in software:
- On a fresh context rounds is 0. With the C implementation the mode
fails with KEYUSAGE_E; with ARM assembly on aarch64 it crashes
(SIGSEGV in AES_CBC_encrypt).
- On a context that held a software key before, rounds and the
schedule were still the old key's. The declined mode then succeeded,
encrypting with the previous key instead of the one just set.
The set-key now drops the old schedule before offering the key to the
device: rounds goes to 0, and the schedule, including the C-fallback and
bitsliced copies, and devKey are wiped so no material from the previous
key outlives the claim. After a claim the context is marked installed
only if the callback built a schedule itself, as the in-tree test
callbacks do by re-running the software set-key. Otherwise a declined
mode returns MISSING_KEY, matching wc_AesInit_Id().
WOLF_CRYPTO_CB_ONLY_AES builds keep the key marked installed, since
there every block goes to the device's ECB callback.
The same wipe also clears the streaming GCM key and the recorded key
length. Otherwise a context that held a software GCM key and was then
re-keyed through wc_AesSetKey() or wc_AesCcmSetKey() kept gcmKeySet, and
the ARM assembly build streamed GCM on the wiped schedule; and a set-key
the device failed left the old key length behind, which
wc_AesGetKeySize() reported as a valid key.
The ARM32 and PPC assembly builds have their own wc_AesSetKey(), which
set keyInstalled on every claim and kept the old schedule. They get the
same reset. On ARM32 a declined CBC on a freshly claimed key returned 0,
with output from the empty schedule. The PPC copy only offers the key
through WOLF_CRYPTO_CB_AES_SETKEY, so it resets on that path alone.
Streaming GCM had the same problem through gcmKeySet, which
wc_AesGcmSetKey() never cleared. After a software GCM key, a key the
device then claimed left the flag set, and with ARM assembly the
streaming API encrypted under the previous key and returned 0.
wc_AesGcmSetKey() now clears gcmKeySet before setting the key and sets
it again only on success. A set-key the device fails returns before
deriving H or copying the key to devKey, so a caller that ignores the
error cannot stream on the wiped schedule. Dropping the key also clears
nonceSet, so a context already set up for a message under the old key
needs a new wc_AesGcmInit() instead of streaming on that key's counter
state.
The flag is set before H is derived, so a failure there, a refused
vector register save in an AES-NI build or a failing wc_AesEncrypt(),
returned an error with gcmKeySet still set and H from the previous key.
That block now clears the flag on failure, as the aarch64 path already
did. Neither failure can be provoked from the API tests.
wc_AesGcmSetKey() also stops skipping H and the devKey copy when devCtx
is set. A key a device owns without a schedule now leaves through the
early return, so devCtx is only still set at that point when the context
has a software schedule: the device built one, or a
WOLF_CRYPTO_CB_AES_SETKEY device declined a re-key and kept its handle
from an earlier claim. In the second case the skip left H all zero, and
the C build produced streaming GCM tags that did not depend on the
ciphertext. The ARM assembly H derivation never checked devCtx.
The wc_CryptoCb_AesSetKey() docs promised a devKey copy and GCM tables
for software fallback in TLS builds, which no build provides, while the
README and the AES CryptoCB key import page promised the opposite with
no conditions. All three now describe what a claimed key leaves behind,
and that wolfCrypt takes a non-zero aes->rounds to mean the callback
built a software schedule. wc_AesGetKeySize() reports aes->keylen for a
key with a length but no rounds, so a callback has no reason to set
aes->rounds just to make key-size queries work. The keyInstalled field
comment in aes.h now says a claimed key counts only with a schedule.
The docs also now say that a callback declining a new key must first
release any key it holds for the object and set aes->devCtx to NULL.
wolfCrypt leaves devCtx alone on a decline, so a callback that kept its
handle went on running the operations it accepts, such as one-shot GCM,
under the old key while the software modes used the new one.
A WOLF_CRYPTO_CB_SETKEY claim now also stores the IV it was given in
aes->reg, as the AES_SETKEY claim and the software set-key already do,
so a device that reads the IV from aes->reg does not get the previous
key's.
test_wc_CryptoCb_Aes_SetKey_Declined claims a key, declines CBC and
expects MISSING_KEY, both on a fresh context and after a software key,
and checks the old key is gone from devKey. It needs
WOLFSSL_AES_REQUIRE_KEY_SET, since without the key-set check the
declined mode fails with KEYUSAGE_E instead.
test_wc_CryptoCb_AesGcm_SetKey covers the same software-then-claimed
sequence for streaming GCM.
test_wc_CryptoCb_AesSetKey checks the same for a key claimed through
WOLF_CRYPTO_CB_AES_SETKEY: CBC, which its device declines, returns
MISSING_KEY.
Both tests also check a set-key the device fails with WC_HW_E: CBC and
streaming GCM then return MISSING_KEY.
test_wc_CryptoCb_AesSetKey has its device release its handle and
decline a re-key to a different key, checks that one-shot GCM then fails
in the callback instead of using the old key, and compares the streaming
GCM tag with one computed in software under the new key.
test_wc_CryptoCb_Aes_SetKey_Declined also checks that a claimed key
takes the IV passed with it and reports its size, and that a failed
set-key reports none. test_wc_CryptoCb_AesGcm_SetKey re-keys a software
GCM key through wc_AesSetKey() and checks that streaming GCM refuses the
claimed key. test_wc_AesGcmStream checks that wc_AesGcmSetKey() on a
context initialized for a message leaves updates waiting for a new
wc_AesGcmInit().
With WOLF_CRYPTO_CB_SETKEY, a callback that returns 0 for WC_SETKEY_AES
owns the key: wc_AesSetKey() builds no key schedule and leaves rounds at
0. wc_AesGcmSetKey() still derived the hash subkey H from that empty
schedule. With the C implementation wc_AesEncrypt() rejects rounds 0 and
the set-key fails with KEYUSAGE_E. On aarch64 with ARM assembly,
AES_ECB_encrypt() runs with rounds 0 and crashes (SIGSEGV). The
function also copied the plaintext key into devKey, which should not stay
behind once a device owns the key.
wc_AesGcmSetKey() now returns as soon as the device has claimed the key,
before deriving H, marking the streaming API keyed or copying to devKey.
On that return it wipes the GHASH subkey a previous key may have left,
and the key unwrapped from a CAAM blob. One-shot GCM reaches the device
through its callback as before.
Streaming GCM has no callback, so it must refuse such a key. The update
calls already return MISSING_KEY while gcmKeySet is clear, but
wc_AesGcmInit() set up the IV without checking it: given a key and an
IV, it ran the counter block through the empty schedule, with the same
KEYUSAGE_E or SIGSEGV as above. It now sets up the IV only once a GCM
key is set. Without one it keeps the IV cached and returns 0, so the
first update returns MISSING_KEY and a later call that sets the key
picks the IV up. That is also what an IV passed before the key relies
on, which on master crashed on aarch64 and failed with KEYUSAGE_E in C.
An IV longer than a block cannot be cached, since it goes through GHASH,
so that case returns MISSING_KEY and leaves the context unchanged.
The same applies to a key claimed through the older
WOLF_CRYPTO_CB_AES_SETKEY hook. The C code already skipped H when the
callback set devCtx, but the ARM assembly derivation did not, so
test_wc_CryptoCb_AesSetKey crashed in AES_ECB_encrypt on aarch64 with
--enable-armasm. Both hooks signal ownership by leaving rounds at 0,
which the XTS entry points already rely on.
test_wc_CryptoCb_AesGcm_SetKey registers a device that claims the key and
performs GCM itself, checks the result against software, and checks that
streaming GCM refuses the key in both wc_AesGcmInit() and the update.
test_wc_CryptoCb_AesSetKey also checks that streaming GCM refuses an
AES_SETKEY claim, and test_wc_AesGcmStream that an IV set before the key
is used once the key arrives, through wc_AesGcmInit() with the key or
the first wc_AesGcmInit() after wc_AesGcmSetKey(), and that a long one
is refused.
wc_AesGcmSetIV(), wc_AesGcmSetExtIV() and wc_AesCcmSetNonce() cache the
nonce in aes->reg, with its length in aes->nonceSz. wc_AesGcmSetKey()
and wc_AesCcmSetKey() then call wc_AesSetKey(), which ends by loading an
IV into aes->reg: an all-zero block for GCM, and zeros for CCM, which
passes no IV. A key set after the nonce therefore left nonceSz in place
and the nonce itself zeroed, and the next operation used an all-zero
nonce:
- wc_AesGcmSetIV(), wc_AesGcmSetKey(), then wc_AesGcmEncrypt_ex() or
wc_AesGcmEncryptInit_ex(): ivOut reports zeros, and every message
under that key gets the same nonce.
- wc_AesGcmSetIV() then wc_AesGcmEncryptInit_ex() with the key: ivOut
reports the random nonce, the data is encrypted under zeros, and
decrypting with ivOut fails.
- wc_AesGcmSetExtIV() then wc_AesGcmInit() with the key and no IV, the
order the wc_AesGcmInit() comment describes.
- wc_AesCcmSetNonce(), wc_AesCcmSetKey(), then wc_AesCcmEncrypt_ex().
A GCM nonce reused under one key exposes the XOR of the plaintexts and
lets the GHASH key be recovered, so tags can be forged. A reused CCM
nonce exposes the XOR of the plaintexts too. EVP and TLS are not
affected: EVP passes the IV again together with the key, and TLS sets
the key before the nonce.
Both set-key functions now hand the cached nonce to wc_AesSetKey() when
one is set, so aes->reg keeps it. The devcrypto wc_AesGcmSetKey() and
the TI AesAuthSetKey(), behind its GCM and CCM set-key, zeroed the nonce
the same way and get the same change. The kcapi and AF_ALG
wc_AesGcmSetKey() never write aes->reg.
The GCM and CCM nonce uniqueness tests now also cache a nonce before
setting the key and check that wc_AesGcmEncrypt_ex() and
wc_AesCcmEncrypt_ex() report it. test_wc_AesGcmStream checks that a
nonce from wc_AesGcmSetIV() followed by wc_AesGcmEncryptInit_ex() with
the key decrypts with the reported ivOut.
FIPS builds at v5.2 or older force WOLFSSL_NO_SHAKE128/256 on in
settings.h, which blocks ML-DSA and ML-KEM's internal SHAKE hashing
under --enable-fips=v5. Adds a CFLAGS opt-in, V523_NONFIPS_SHAKE,
that restores SHAKE128/256 for exactly the cert4718 (5.2.1) build.
- configure.ac, src/include.am, wolfssl/wolfcrypt/include.am: build
and install the paired shake.c/shake.h from the internal fips repo
overlay only when the opt-in and FIPS version match, validating
both files exist first
- hash.c, asn.c, wc_mldsa.c/.h, benchmark.c, test.c, test_sha3.c:
include shake.h ahead of their first sha3.h/asn.h include, since
asn.h can reach sha3.h unguarded via ed448.h/wc_lms.h/wc_xmss.h
first
- wc_mlkem.h: the one exception - its override must come after its
sha3.h include, since sha3.h processes first there with SHAKE off,
and shake.h's own fallback typedef and prototypes are what actually
declare wc_Shake
- every override site uses the same
defined(WOLFSSL_SHA3) && defined(V523_NONFIPS_SHAKE) guard
- registers V523_NONFIPS_SHAKE in .wolfssl_known_macro_extras, which
check-source-text otherwise flags as unrecognized
A second review of the close_notify changes found:
- An SNI callback returning warning_return made a TLS 1.3 server send
an alert at warning level and carry on with the handshake. TLS 1.3
error alerts are always fatal (RFC 8446 Section 6.2), so the previous
commit marked the connection fatally closed and the server then sent
its whole flight on a connection that refused every write. The
client had already given up, as it treats that alert as fatal too.
warning_return is what OpenSSL-compatible callbacks return as
SSL_TLSEXT_ERR_ALERT_WARNING to mean "carry on without acknowledging
the name", and OpenSSL handles it in TLS 1.3 by sending no alert and
continuing. Do the same. That was the only TLS 1.3 path sending a
warning-level error alert, so SendAlert_ex() goes back to checking
the level alone.
- A callback that leaves the alert description unset left it at 0,
close_notify, unless OPENSSL_EXTRA and WOLFSSL_EXTRA_ALERTS made it
unrecognized_name. fatal_return then sent a fatal close_notify, and a
TLS 1.2 warning_return sent a warning close_notify, which ends the
connection the callback asked to continue. Send handshake_failure for
fatal_return and no alert for warning_return instead, which still
does not tell the client that the name was unknown.
- With WOLFSSL_RW_THREADED, a KeyUpdate request read before our
close_notify left sendKeyUpdate set. SendData() skipped it without
clearing it, wolfSSL_dtls13_pending_work() kept reporting work and
wolfSSL_dtls13_do_scheduled_work() sent the KeyUpdate after our
close_notify. The response is now dropped once the write side is
closed, and pending_work() no longer reports it.
- The CertificateRequest check returned before parsing, so a malformed
post-handshake request after close_notify was accepted silently and
the trace was left unbalanced. The message is now validated and only
ignored just before its context would be stored.
Merge the close_notify and fatal alert refusals in SendData(), which
differed only in the error code, and drop a comment that described
ssl->error rather than sentNotify.
Move test_dtls13_no_key_update_after_close_notify to the dtls group,
add test_dtls13_scheduled_work_after_shutdown, and replace
test_tls13_sent_warning_error_alert_is_fatal with
test_tls_sni_callback_alerts covering TLS 1.3 and TLS 1.2, warning and
fatal returns, with and without an alert description. Keep two test
comments within 80 columns.
Review of the previous commit found more records, state changes and
misleading errors after our close_notify or a fatal alert:
- A TLS 1.2 server that sent close_notify still reset its handshake
state for a renegotiation ClientHello, and with
WOLFSSL_OP_NO_RENEGOTIATION, or when a DTLS epoch would wrap, still
sent a no_renegotiation alert. It now skips the message, as the
client does for a HelloRequest.
- An ignored post-handshake CertificateRequest was still parsed, and
its context stayed linked on ssl->certReqCtx, so a peer could grow
that list without bound while we waited for its close_notify. The
message is now skipped before parsing.
- Dtls13DoScheduledWork() still sent the KeyUpdate that
Dtls13CheckAEADFailLimit() schedules after too many failed
decryptions.
- SendAlert_ex() only marked the connection closed for alert_fatal.
In TLS 1.3 every alert except close_notify and user_canceled is
fatal, which DoAlert() already applies on receipt. An SNI callback
returning warning_return sends such an alert, after which the server
could still send 0.5-RTT application data.
- A refused write reported the positive WOLFSSL_ERROR_WANT_READ, which
wolfSSL_peek() leaves behind after a post-handshake message, instead
of SSL_SHUTDOWN_ALREADY_DONE_E or SOCKET_PEER_CLOSED_E, so an event
loop would wait for readability and retry forever.
Add IsWriteClosed() for the sentNotify || fatalAlert check that now
guards every one of these paths.
Extend test_tls12_no_renegotiation_after_close_notify (handshake state
and WOLFSSL_OP_NO_RENEGOTIATION) and
test_tls13_no_pha_response_after_close_notify (certReqCtx), and add
test_tls13_refused_write_after_peek,
test_dtls13_no_key_update_after_close_notify and
test_tls13_sent_warning_error_alert_is_fatal.
The previous commit stopped wolfSSL_write() from building handshake or
KeyUpdate records after a fatal alert or our close_notify. Records the
peer provokes still went out after that point:
- wolfSSL_negotiate() in ReceiveData(), including the secure
renegotiation one in its loop. After wolfSSL_SendUserCanceled()
cancelled a handshake, a wolfSSL_read() still finished the handshake
and sent the Finished.
- DoTls13KeyUpdate() answering a peer's update_requested. This is
reached from wolfSSL_read() and from the wolfSSL_shutdown() loop
waiting for the peer's close_notify.
- A post-handshake CertificateRequest, answered with Certificate,
CertificateVerify and Finished from inside ProcessReply().
- A TLS 1.2 HelloRequest, which started a renegotiation from
ReceiveData().
- The write side of a write duplicate sending a PHA response or
KeyUpdate delegated by the read side.
Skip all of them once sentNotify or fatalAlert is set. A peer KeyUpdate
still moves our receive keys forward, so a half-closed connection keeps
reading.
Extend test_tls13_no_handshake_after_user_canceled with a read, and add
test_tls13_no_key_update_response_after_close_notify (read and
shutdown), test_tls13_no_pha_response_after_close_notify,
test_tls13_write_dup_no_response_after_close_notify and
test_tls12_no_renegotiation_after_close_notify (HelloRequest and
ClientHello).
SendData() refuses new application data once a fatal alert was seen or
our close_notify was sent, but wolfSSL_negotiate() and the
WOLFSSL_RW_THREADED KeyUpdate response ran before that check. After
wolfSSL_SendUserCanceled() cancelled a handshake, a wolfSSL_write()
still finished the handshake and sent the Finished after the
close_notify, and a KeyUpdate response queued by the read side went out
the same way.
Skip both once sentNotify or fatalAlert is set. The refusal stays after
SendBuffered() and RetrySendAlert(), so a close_notify left in the
output buffer by WANT_WRITE is still flushed.
Add test_tls13_no_handshake_after_user_canceled and, for
WOLFSSL_RW_THREADED builds, test_tls13_no_key_update_after_close_notify.
A write refused after our close_notify leaves SSL_SHUTDOWN_ALREADY_DONE_E
in ssl->error, and ProcessReply() refuses to run in that state, so the
second wolfSSL_shutdown() waiting for the peer's close_notify failed.
Clear the error in wolfssl_shutdown_recv_close_notify() before reading,
as ReceiveData() already does. Keeping the error, rather than not setting
it, lets wolfSSL_get_error() report why the write failed in builds that
have no error queue.
Add test_tls13_shutdown_after_refused_write, wrap the expected error
codes of the new tests in WC_NO_ERR_TRACE(), and drop the version nibble
rewrite in test_dtls12_export_import_v6 that test_export_options_v6()
already does.
The session cache is shared by every CTX in the process, and a
connection between a client and a server in the same process stores two
entries under the same session ID, one per side. Both landed in the same
row, so each connection used two of the row's three slots in the default
cache. When the next connection's ID hashed to the same row, its two
inserts wrapped around and overwrote the oldest entry of the first
connection.
Since the client caches its session in DoFinished(), after the server
verified the Finished, the oldest entry is the server's, so the server
could no longer resume the first session. That made test_session and
test_session_handling in the CPython CI fail intermittently with
session_reused False.
Hash client entries into the row after the one their ID maps to, so the
two sides of a connection never compete for the same row. This changes
where a persisted cache keeps its client entries, but
WOLFSSL_CACHE_VERSION 4 has not been in a release yet, so it covers the
new mapping and caches saved by released builds (version 3) are still
rejected on restore.
The new test only builds with three or fewer sessions per row, since two
connections cannot evict anything from a larger row, and reports a skip
when it finds no pair of connections sharing a row.
SendData() refused to write whenever options.isClosed was set, to stop
application data after a fatal alert (RFC 8446 Section 6.2). isClosed is
also set when the transport reports a plain EOF, so a server could no
longer answer a request whose client had already closed its socket.
The write failed without reaching the socket, which reports
SSL_ERROR_SYSCALL with a stale errno instead of EPIPE or ECONNRESET.
nginx logs that at crit level, which made the ssl_stapling.t "no crit"
check in the nginx CI fail intermittently.
Track a fatal alert, sent or received, in a new options.fatalAlert bit
and check that in SendData() instead. A transport EOF goes back to
letting the write reach the socket, as it did before.
Also refuse writes once our own close_notify has been sent, as OpenSSL
does. The isClosed check only caught that when an EOF happened to
arrive after the close_notify. The refused write sets
SSL_SHUTDOWN_ALREADY_DONE_E, which ReceiveData() clears, so the
application can still read the peer's remaining data and its
close_notify.
Session export writes fatalAlert as its own options byte. Export version
7 has not been in a release yet, so it takes the byte without a new
version number; version 7 blobs written by master before this change no
longer import. A version 6 blob only has isClosed, so the importer takes
it as the fatal alert state too, as SendData() did before.
test_wc_LmsFeatureCoverage ran the SHA-256/256 round trip
unconditionally and made an L2/H5/W8 key, which only the SHA-256/256
family has, so --group lms failed with --enable-lms=...,no-sha256-256.
Guard both on !WOLFSSL_NO_LMS_SHA256_256, as the other SHA-256/256
tests in the file are.
--enable-lms=shake256 did not turn on SHA-3 and SHAKE256, so a build
without them failed to compile. Turn both on for it in configure, as
for ML-KEM, ML-DSA, FrodoKEM and Falcon, and for WOLFSSL_LMSSHAKE256
in CMake, as for WOLFSSL_FALCON.
The test returned TEST_SUCCESS whatever its checks found, so the
failing shutdown check was never reported. With session tickets
enabled, the client's second wolfSSL_shutdown() processed the server's
unread NewSessionTicket and returned before the server's close_notify,
and once that is drained the server's second call stops at the
client's ACK the same way.
Read once on each side before shutting down, so each second
wolfSSL_shutdown() finds the peer's close_notify, and return
EXPECT_RESULT().
The point formats extension stored its formats as a linked list with one heap
node per one byte value, which is the shape the supported groups extension had
before. A node costs sixteen bytes to hold one.
Replace the list with a block holding a count, a capacity and a flat array,
matching what the supported groups extension does now. The block starts with
room for four formats, which is more than are defined, and doubles if a caller
adds more.
TLSX_UseMaxFragment() asked the allocator for a single byte to hold the length
code the peer is told about, and freed it when the extension went away. The
TLSX object already carries a value field for extensions whose data is a
scalar, which is how the PSK key exchange modes and early data extensions
store theirs, so use that and hold no data at all.
TLSX_TCA_New() allocated the object, then the identifier, and freed the object
again on every path where the identifier was missing or the wrong size. Check
the type and size first and put the identifier in a flexible array at the end
of the object, so the failure paths are early returns and there is one buffer
to free.
TLSX_TCA_New() never makes a SHA-1 or X509 name entry without an identifier,
so TLSX_TCA_Write() drops its fallbacks for a missing one.
TLSX_SNI_New() allocated the object and then the host name it points at, so
every server name costs two allocations and two frees. Put the name in a
flexible array at the end of the object instead. A union cannot hold one, so
the single member data union goes and the name is sni->host_name.
The host name is the only SNI type there is, so the switch that picked the
allocation to make becomes an early return for any other type, which is what
the default case did anyway.
wc_SlhDsaKey_SignHashWithRandom() returned NOT_COMPILED_IN for
WC_HASH_TYPE_NONE, and so did wc_SlhDsaKey_SignHashDeterministic(),
which calls it. wc_SlhDsaKey_SignHash() and wc_SlhDsaKey_VerifyHash()
return BAD_FUNC_ARG for the same input from an identical check, and
the English doxygen for all four says BAD_FUNC_ARG. Return
BAD_FUNC_ARG here too, and check both functions for it in
test_wc_slhdsa_sign_hash.
Add the hashType BAD_FUNC_ARG line to the Japanese doxygen of the four
functions, as in the English. Fix three "NOT_COMPILED in" typos in the
function comments, and a SignHash() comment that said a bad hashSz
still draws from the DRBG.
Add the X448 inputs that give an all-zero shared secret (u = 0, 1 and
p-1, and the non-canonical encodings p and p+1) to
test_tls13_mlkem_bad_key_share: in the X448 part of an X448MLKEM768
ClientHello and ServerHello, and in a plain X448 ServerHello.
wc_curve448_check_public() rejects them, so each must fail with
ECC_PEERKEY_ERROR and a fatal illegal_parameter alert.
Also run X448MLKEM768 through the same length and ML-KEM key checks
as the other hybrid groups.