Skip to content

Commit c1b0276

Browse files
committed
Zero TLS 1.3 traffic keys after AES SE offload
When WOLF_CRYPTO_CB_AES_SETKEY is enabled and a CryptoCB callback imports the AES key into a Secure Element (aes->devCtx != NULL), the TLS-layer copy in keys->{client,server}_write_key has no further consumer: the software key schedule is not populated on offload. ForceZero it in SetKeysSide() per provisioned side. The static IVs (keys->{client,server}_write_IV and keys->aead_{enc,dec}_imp_IV) are left intact because BuildTls13Nonce() reads aead_{enc,dec}_imp_IV on every record (RFC 8446 Section 5.3). Scope: TLS 1.3, non-DTLS, non-QUIC. DTLS 1.3 needs the write keys in Dtls13EpochCopyKeys; TLS 1.2 needs them for rehandshake; QUIC is untouched pending audit. Add two memio tests (test_wc_CryptoCb_Tls13_Key_{Zero_After_Offload, No_Zero_Without_Offload}) that pin AES-GCM and check key / IV state after the handshake and a KeyUpdate round. Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
1 parent f086e91 commit c1b0276

File tree

4 files changed

+502
-1
lines changed

4 files changed

+502
-1
lines changed

ChangeLog.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# wolfSSL Release (unreleased)
2+
3+
## Enhancements
4+
5+
* TLS 1.3: zero traffic key staging buffers in `SetKeysSide()` once a
6+
CryptoCB callback has imported the AES key into a Secure Element
7+
(`aes->devCtx != NULL`). Clears `keys->{client,server}_write_key`
8+
on the provisioned side(s) after cipher init succeeds. The static
9+
IV buffers (`keys->{client,server}_write_IV`,
10+
`keys->aead_{enc,dec}_imp_IV`) are intentionally left intact because
11+
`BuildTls13Nonce()` reads them on every AEAD record to construct the
12+
per-record nonce. Scoped to TLS 1.3, non-DTLS, non-QUIC; requires
13+
`WOLF_CRYPTO_CB` and `WOLF_CRYPTO_CB_AES_SETKEY`.
14+
115
# wolfSSL Release 5.9.1 (Apr. 8, 2026)
216

317
Release 5.9.1 has been developed according to wolfSSL's development and QA

src/keys.c

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3591,6 +3591,75 @@ int SetKeysSide(WOLFSSL* ssl, enum encrypt_side side)
35913591
ssl->heap, ssl->devId, ssl->rng, ssl->options.tls1_3);
35923592
}
35933593

3594+
/* Zero the TLS-layer staging key buffers once the CryptoCB callback
3595+
* has imported the key into a Secure Element.
3596+
*
3597+
* Convention: after a successful wc_AesSetKey / wc_AesGcmSetKey where
3598+
* the CryptoCB handled the key import, the callback leaves
3599+
* aes->devCtx != NULL and the software key schedule (aes->key,
3600+
* aes->devKey, aes->gcm.H / aes->gcm.M0) is NOT populated. The TLS
3601+
* layer may therefore destroy its staging copy of the traffic key.
3602+
*
3603+
* Only the key buffers (client_write_key / server_write_key) are
3604+
* zeroed. The static IVs (client_write_IV / server_write_IV) and
3605+
* the AEAD implicit-IV copies (aead_{enc,dec}_imp_IV) are NOT
3606+
* zeroed: BuildTls13Nonce() in tls13.c reads keys->aead_*_imp_IV on
3607+
* every AEAD record to construct the per-record nonce
3608+
* (nonce = static_iv XOR seq_num, RFC 8446 Section 5.3). Zeroing
3609+
* them would break the record path or, if applied symmetrically on
3610+
* both peers, silently degenerate the nonce to the bare sequence
3611+
* number and break interop with any unpatched peer. The static_iv
3612+
* is not a confidentiality-critical secret in the same sense as
3613+
* the traffic key; losing it does not compromise plaintext.
3614+
*
3615+
* Scope:
3616+
* - TLS 1.3 only. TLS 1.2 additionally reads
3617+
* keys->{client,server}_write_key for rehandshake/secure
3618+
* renegotiation flows.
3619+
* - Non-DTLS. Dtls13EpochCopyKeys (called from Dtls13NewEpoch)
3620+
* references keys->*_write_key for epoch switching; DTLS 1.3
3621+
* needs separate analysis.
3622+
* - Non-QUIC. QUIC traffic secrets live outside these buffers
3623+
* but the interaction with stack-installed QUIC handlers has
3624+
* not been audited; exclude until it is.
3625+
*
3626+
* When called with ENCRYPT_SIDE_ONLY or DECRYPT_SIDE_ONLY, only the
3627+
* buffer consumed by this call is zeroed; the complementary buffer
3628+
* is written in a later SetKeysSide() from its own DeriveTls13Keys()
3629+
* and StoreKeys() pair (StoreKeys gates on PROVISION_CLIENT /
3630+
* PROVISION_SERVER so only the provisioned side is written).
3631+
*
3632+
* Ordering: this block must run AFTER SetKeys() (so offload has
3633+
* happened) and BEFORE Dtls13SetRecordNumberKeys() /
3634+
* wolfSSL_quic_keys_active() below, in case a future refactor in
3635+
* either starts reading keys->*_write_key. The DTLS and QUIC gates
3636+
* in this block mean neither currently executes on the same ssl,
3637+
* but keep the order explicit. */
3638+
#if defined(WOLF_CRYPTO_CB) && defined(WOLF_CRYPTO_CB_AES_SETKEY)
3639+
if (ret == 0 && ssl->options.tls1_3 && !ssl->options.dtls
3640+
&& !WOLFSSL_IS_QUIC(ssl)) {
3641+
int encOffloaded = (wc_encrypt != NULL && wc_encrypt->aes != NULL &&
3642+
wc_encrypt->aes->devCtx != NULL);
3643+
int decOffloaded = (wc_decrypt != NULL && wc_decrypt->aes != NULL &&
3644+
wc_decrypt->aes->devCtx != NULL);
3645+
3646+
if (encOffloaded || decOffloaded) {
3647+
if (ssl->options.side == WOLFSSL_CLIENT_END) {
3648+
if (encOffloaded)
3649+
ForceZero(keys->client_write_key, ssl->specs.key_size);
3650+
if (decOffloaded)
3651+
ForceZero(keys->server_write_key, ssl->specs.key_size);
3652+
}
3653+
else {
3654+
if (encOffloaded)
3655+
ForceZero(keys->server_write_key, ssl->specs.key_size);
3656+
if (decOffloaded)
3657+
ForceZero(keys->client_write_key, ssl->specs.key_size);
3658+
}
3659+
}
3660+
}
3661+
#endif /* WOLF_CRYPTO_CB && WOLF_CRYPTO_CB_AES_SETKEY */
3662+
35943663
#ifdef WOLFSSL_DTLS13
35953664
if (ret == 0 && ssl->options.dtls && IsAtLeastTLSv1_3(ssl->version))
35963665
ret = Dtls13SetRecordNumberKeys(ssl, side);

0 commit comments

Comments
 (0)