TLS 1.3 application traffic secrets survive public session export after connection teardown
Summary
The original suspicion is real, but it is a conditional key-lifecycle/security issue, not a TLS wire-interoperability failure or a strict MUST violation. The generated requirement incorrectly labeled the RFC text as MUST: RFC 9846 Appendix F describes the erasure condition for claiming forward secrecy, while Section 7.1 gives the directly applicable normative strength as SHOULD.
mbedTLS does erase several TLS 1.3 ephemeral and handshake materials: the generated key-share private key, raw shared secret, handshake traffic secret buffers, transcript buffers, and TLS 1.3 master-secret structure are cleaned on the checked path.
The remaining confirmed gap is on the client public session-export path. client_application_traffic_secret_N and server_application_traffic_secret_N are stored in mbedtls_ssl_session.app_secrets; after processing a valid NewSessionTicket, mbedtls_ssl_get_session() copies them into a caller-owned session intended for later resumption. A strengthened runtime probe shows that both traffic secrets remain non-zero in that exported object even after the original TLS connection has been freed. They disappear only when the caller explicitly frees the exported session.
A follow-up key-recovery rerun on 2026-08-11 went beyond buffer-liveness and directly recreated both TLS 1.3 application traffic transforms from the exported client session after connection teardown. Using only the retained app_secrets copy plus the negotiated ciphersuite, the probe successfully decrypted one captured client->server application record and one captured server->client application record from the original connection.
Scope notes:
- Retaining the current traffic secret while a live connection needs it for KeyUpdate would be legitimate. This mbedTLS version does not implement TLS 1.3 KeyUpdate handling, and the two fields have no post-handshake consumer in the reviewed code.
mbedtls_ssl_get_session() is client-only. The server has the same fields in its internal session, and the internal copy helper copies them, but this report no longer claims a public server export path.
- TLS 1.3 session serialization does not write these application traffic secrets to the serialized buffer. The confirmed exposure is the caller-owned in-memory
mbedtls_ssl_session object.
The second-pass runtime recheck completed a TLS 1.3 handshake, processed a server NewSessionTicket, exported the client session with the public API, freed the original TLS connection, and then inspected whether the exported session still contained non-zero application traffic secrets.
Standard Requirement
Official standard:
Section 7.1 gives the directly applicable normative rule:
Once all the values which are to be derived from a given secret have
been computed, that secret SHOULD be erased.
Section 7.2 explains the valid reason for keeping the current generation while KeyUpdate is supported, and when an old generation should be deleted:
Once client_/server_application_traffic_secret_N+1 and its associated
traffic keys have been computed, implementations SHOULD delete
client_/server_application_traffic_secret_N and its associated traffic
keys.
Appendix F describes the security consequence and the erasure condition for claiming forward secrecy:
keys derived in the TLS key schedule other than binder_key,
resumption_secret, and PSKs derived from the resumption_secret also
need to be erased
The generated task snapshot called this a MUST, but neither the quoted Appendix F sentence nor the more specific Section 7.1 rule has MUST strength. The correct standards claim is therefore a SHOULD-level key-lifecycle issue plus a failure to meet Appendix F's erasure condition for the exported object's lifetime.
TLS 1.3 client_application_traffic_secret_N and server_application_traffic_secret_N are key-schedule outputs and are not among the Appendix F exceptions. Keeping a current generation inside a live connection can be necessary for KeyUpdate. Copying it into a resumption session that outlives the connection is not needed for traffic protection, KeyUpdate, ticket processing, session serialization, or resumption.
Relevant Source Code
Positive cleanup paths:
implementions/mbedtls-development/library/ssl_tls13_keys.c:1467-1474 destroys the internally generated key-share private key after raw agreement.
implementions/mbedtls-development/library/ssl_tls13_keys.c:1500-1501 zeroizes and frees the raw shared secret buffer.
implementions/mbedtls-development/library/ssl_tls13_keys.c:1640-1644 zeroizes handshake random bytes and transcript after deriving application keys.
implementions/mbedtls-development/library/ssl_tls13_keys.c:1727-1729 zeroizes the TLS 1.3 master-secret structure after deriving the resumption master secret.
implementions/mbedtls-development/library/ssl_tls.c:4573-4596 eventually zeroizes the whole mbedtls_ssl_session object during session free.
Application traffic secrets are stored in session state:
mbedtls_ssl_tls13_application_secrets * const app_secrets =
&ssl->session_negotiate->app_secrets;
implementions/mbedtls-development/library/ssl_tls13_keys.c:1531-1533
The first application traffic secrets are then used to derive record-layer keys:
app_secrets->client_application_traffic_secret_N,
app_secrets->server_application_traffic_secret_N,
implementions/mbedtls-development/library/ssl_tls13_keys.c:1587-1591
The session type embeds the full TLS 1.3 application-secret structure:
unsigned char client_application_traffic_secret_N[MBEDTLS_TLS1_3_MD_MAX_SIZE];
unsigned char server_application_traffic_secret_N[MBEDTLS_TLS1_3_MD_MAX_SIZE];
implementions/mbedtls-development/include/mbedtls/ssl.h:1085-1090
mbedtls_ssl_tls13_application_secrets MBEDTLS_PRIVATE(app_secrets);
implementions/mbedtls-development/include/mbedtls/ssl.h:1239-1240
Session copy duplicates the whole structure:
memcpy(dst, src, sizeof(mbedtls_ssl_session));
implementions/mbedtls-development/library/ssl_tls.c:222-226
mbedtls_ssl_get_session() calls that copy path:
ret = mbedtls_ssl_session_copy(dst, ssl->session);
implementions/mbedtls-development/library/ssl_tls.c:3024-3051
The API documentation defines the destination as an exported session for later resumption and serialization:
* \param session The target structure in which to store the exported session.
...
* session resumption by passing it to mbedtls_ssl_set_session(),
* and serialized for storage via mbedtls_ssl_session_save().
implementions/mbedtls-development/include/mbedtls/ssl.h:4770-4788
The public export function is guarded by MBEDTLS_SSL_CLI_C and rejects non-client endpoints (ssl_tls.c:3023-3033). Thus only the client public export path is confirmed.
There is no TLS 1.3 KeyUpdate processing path in this version. Post-handshake processing accepts NewSessionTicket on clients and returns MBEDTLS_ERR_SSL_UNEXPECTED_MESSAGE for all other TLS 1.3 post-handshake messages:
if (ssl_tls13_is_new_session_ticket(ssl)) {
...
}
...
/* Fail in all other cases. */
return MBEDTLS_ERR_SSL_UNEXPECTED_MESSAGE;
implementions/mbedtls-development/library/ssl_msg.c:5459-5485
Implementation Behavior
For TLS 1.3, mbedTLS derives application secrets from the master secret and handshake transcript into ssl->session_negotiate->app_secrets. It then derives application write keys and IVs from client_application_traffic_secret_N and server_application_traffic_secret_N.
After transform population and the optional key-export callback, the code zeroizes temporary random/transcript buffers, but not the two application traffic secret fields. During TLS 1.3 handshake wrapup, session_negotiate becomes the active session (implementions/mbedtls-development/library/ssl_tls13_generic.c:1250-1269).
The codebase-wide references to the two fields are limited to derivation, initial traffic-key creation, debug/key-export callback output, structure declarations, and tests. They are not used for post-handshake record protection or KeyUpdate. Record protection uses keys in the active transform, while TLS 1.3 post-handshake dispatch has no KeyUpdate path.
On the client, mbedtls_ssl_get_session() then copies the whole session with memcpy, including both traffic secrets, into caller-owned resumption state. Freeing the original mbedtls_ssl_context does not affect that independent copy. Final cleanup does exist: mbedtls_ssl_session_free() zeroizes the copied object. The gap is therefore unnecessary retention from public export until caller cleanup, not total absence of cleanup.
Additional boundary check: TLS 1.3 session serialization in implementions/mbedtls-development/library/ssl_tls.c:3435-3573 serializes ticket and resumption state, not the full app_secrets structure. The concrete issue demonstrated here is the in-memory client API export, especially when that caller-owned object outlives the source connection.
Inconsistency Reason
RFC9846 Section 7.1 says a secret SHOULD be erased once all values to be derived from it have been computed. Appendix F says that failing to erase ephemeral or connection-specific key-schedule material prevents the erasure condition underlying forward secrecy. mbedTLS satisfies this objective for the checked ephemeral private key, shared secret, handshake secrets, and master-secret cleanup paths.
The implementation is incomplete for exported client_application_traffic_secret_N and server_application_traffic_secret_N: both are connection-specific key-schedule outputs capable of deriving application record-layer keys; neither is needed by TLS 1.3 resumption; this version has no KeyUpdate consumer; yet the client public session-export API copies both into state designed to outlive the connection.
This is a real, conditional security issue and a SHOULD-level standards-alignment gap. It is conditional because applications that never call mbedtls_ssl_get_session(), or that serialize and immediately free the exported object, do not retain this extra post-connection copy. It is not correctly described as a strict RFC MUST violation.
Runtime Evidence
The focused probe was strengthened to exercise the normal TLS 1.3 resumption-ticket workflow:
- complete a TLS 1.3 handshake,
- have the server send a real
NewSessionTicket,
- have the client process that ticket,
- call the public client API
mbedtls_ssl_get_session(),
- free the original TLS connection,
- inspect the independent exported
mbedtls_ssl_session,
- finally call
mbedtls_ssl_session_free() on the exported session.
The probe was recompiled and rerun on 2026-08-10 for this second-pass review. It exercised the normal TLS 1.3 ticket-based resumption workflow, exported the client session through mbedtls_ssl_get_session(), freed the original TLS connection, and then checked the independent exported mbedtls_ssl_session.
Result: exit code 0; the second-pass log includes run_exit_code_observed_by_shell=0.
The probe covers both forward-secret long-term-key cases called out by RFC9846. In the log, 1 means the checked buffer was all-zero and 0 means non-zero bytes remained.
| Scenario |
Ticket processed |
Public export |
Client app traffic secret after connection free |
Server app traffic secret after connection free |
Exported session free clears app secrets |
cert_ephemeral |
-31488 |
0 |
0 |
0 |
1 |
psk_ephemeral |
-31488 |
0 |
0 |
0 |
1 |
Key second-pass log lines:
scenario=cert_ephemeral begin
cert_ephemeral.handshake_complete iteration=19
cert_ephemeral.client.version=TLSv1.3
cert_ephemeral.server.version=TLSv1.3
cert_ephemeral.client.tls13_master_secrets_zero=1
cert_ephemeral.client.tls13_hs_secrets_zero=1
cert_ephemeral.client.randbytes_zero=1
cert_ephemeral.client.session_client_application_traffic_secret_zero=0
cert_ephemeral.client.session_server_application_traffic_secret_zero=0
cert_ephemeral.server.post_handshake_flush_ret=0 queued=165
cert_ephemeral.client.process_new_session_ticket_ret=-31488
cert_ephemeral.client.public_get_session_ret=0
cert_ephemeral.client.exported_session_client_application_traffic_secret_zero=0
cert_ephemeral.client.exported_session_server_application_traffic_secret_zero=0
cert_ephemeral.client.exported_session_app_secrets_zero_after_connection_free=0
cert_ephemeral.client.exported_session_client_application_traffic_secret_zero_after_connection_free=0
cert_ephemeral.client.exported_session_server_application_traffic_secret_zero_after_connection_free=0
cert_ephemeral.client.exported_session_app_secrets_zero_after_session_free=1
scenario=cert_ephemeral end ret=0
scenario=psk_ephemeral begin
psk_ephemeral.handshake_complete iteration=15
psk_ephemeral.client.version=TLSv1.3
psk_ephemeral.server.version=TLSv1.3
psk_ephemeral.client.tls13_master_secrets_zero=1
psk_ephemeral.client.tls13_hs_secrets_zero=1
psk_ephemeral.client.randbytes_zero=1
psk_ephemeral.client.session_client_application_traffic_secret_zero=0
psk_ephemeral.client.session_server_application_traffic_secret_zero=0
psk_ephemeral.server.post_handshake_flush_ret=0 queued=165
psk_ephemeral.client.process_new_session_ticket_ret=-31488
psk_ephemeral.client.public_get_session_ret=0
psk_ephemeral.client.exported_session_client_application_traffic_secret_zero=0
psk_ephemeral.client.exported_session_server_application_traffic_secret_zero=0
psk_ephemeral.client.exported_session_app_secrets_zero_after_connection_free=0
psk_ephemeral.client.exported_session_client_application_traffic_secret_zero_after_connection_free=0
psk_ephemeral.client.exported_session_server_application_traffic_secret_zero_after_connection_free=0
psk_ephemeral.client.exported_session_app_secrets_zero_after_session_free=1
scenario=psk_ephemeral end ret=0
run_exit_code_observed_by_shell=0
The strengthened result closes both ambiguities in the earlier probe: it observes the intended ticket-export workflow rather than exporting before a ticket exists, and it checks after connection teardown rather than only during an active connection. Handshake/master temporary material is zeroized. More importantly, the client public API exports both application traffic secrets, and those secrets remain non-zero after the source TLS connection has been destroyed. They are zeroized only by mbedtls_ssl_session_free() on the caller-owned object.
A follow-up rerun on 2026-08-11 used the same probe source with added recovery logic and logged to manual_recheck_runtime/tls13_key_lifecycle_probe.key_recovery_20260811.log. That rerun captured one real application-data record in each direction before teardown, freed the original TLS connection, rebuilt the corresponding TLS 1.3 application transforms solely from the exported mbedtls_ssl_session.app_secrets, and then decrypted both captured records successfully.
| Scenario |
Client->server old-record decrypt after connection free |
Server->client old-record decrypt after connection free |
cert_ephemeral |
1 |
1 |
psk_ephemeral |
1 |
1 |
Key recovery log lines:
cert_ephemeral.decrypt_client_to_server_after_connection_free.decrypt_ret=0
cert_ephemeral.decrypt_client_to_server_after_connection_free.decrypted_content_type=23
cert_ephemeral.decrypt_client_to_server_after_connection_free.decrypted_plaintext_len=23
cert_ephemeral.decrypt_client_to_server_after_connection_free.decrypted_plaintext_matches=1
cert_ephemeral.client_to_server_old_key_recovery_after_connection_free=1
cert_ephemeral.decrypt_server_to_client_after_connection_free.decrypt_ret=0
cert_ephemeral.decrypt_server_to_client_after_connection_free.decrypted_content_type=23
cert_ephemeral.decrypt_server_to_client_after_connection_free.decrypted_plaintext_len=23
cert_ephemeral.decrypt_server_to_client_after_connection_free.decrypted_plaintext_matches=1
cert_ephemeral.server_to_client_old_key_recovery_after_connection_free=1
psk_ephemeral.decrypt_client_to_server_after_connection_free.decrypt_ret=0
psk_ephemeral.decrypt_client_to_server_after_connection_free.decrypted_content_type=23
psk_ephemeral.decrypt_client_to_server_after_connection_free.decrypted_plaintext_len=23
psk_ephemeral.decrypt_client_to_server_after_connection_free.decrypted_plaintext_matches=1
psk_ephemeral.client_to_server_old_key_recovery_after_connection_free=1
psk_ephemeral.decrypt_server_to_client_after_connection_free.decrypt_ret=0
psk_ephemeral.decrypt_server_to_client_after_connection_free.decrypted_content_type=23
psk_ephemeral.decrypt_server_to_client_after_connection_free.decrypted_plaintext_len=23
psk_ephemeral.decrypt_server_to_client_after_connection_free.decrypted_plaintext_matches=1
psk_ephemeral.server_to_client_old_key_recovery_after_connection_free=1
Impact
The retained application traffic secrets are connection-specific TLS 1.3 key-schedule outputs. Copying them through mbedtls_ssl_get_session() can extend their lifetime beyond connection teardown and place them in caller-managed session objects that applications retain for resumption.
This is no longer only an inference from residual secret bytes: the 2026-08-11 rerun directly used the exported session object to reconstruct the old connection's client and server application traffic transforms and decrypt captured records after the original connection had already been freed. A serialized TLS 1.3 session is not affected because mbedtls_ssl_session_save() omits these fields. Applications that promptly serialize and free the object also bound the exposure. Applications that retain the in-memory session inherit an unnecessary connection-traffic secret alongside the resumption material.
Fix Direction
Split resumable session state from connection-specific application traffic state, or scrub connection-only fields in the client export path.
Keep only lifecycle-required values in exported session objects:
- Keep
resumption_master_secret and resumption-derived ticket PSKs for resumption.
- Keep
exporter_master_secret only while exporter APIs require it, or provide an explicit cleanup or disable path.
- Do not expose
client_application_traffic_secret_N or server_application_traffic_secret_N through mbedtls_ssl_get_session(). If changing the internal copy helper would break live-session uses, explicitly zero these two fields in the exported destination before returning.
After successful application transform population and any configured key-export callback, zeroize client_application_traffic_secret_N and server_application_traffic_secret_N when they are no longer needed for record protection or KeyUpdate. If future KeyUpdate support requires retaining a traffic secret, keep it in connection transform state with a tightly scoped lifetime, not in exported resumable session state.
TLS 1.3 application traffic secrets survive public session export after connection teardown
Summary
The original suspicion is real, but it is a conditional key-lifecycle/security issue, not a TLS wire-interoperability failure or a strict
MUSTviolation. The generated requirement incorrectly labeled the RFC text asMUST: RFC 9846 Appendix F describes the erasure condition for claiming forward secrecy, while Section 7.1 gives the directly applicable normative strength asSHOULD.mbedTLS does erase several TLS 1.3 ephemeral and handshake materials: the generated key-share private key, raw shared secret, handshake traffic secret buffers, transcript buffers, and TLS 1.3 master-secret structure are cleaned on the checked path.
The remaining confirmed gap is on the client public session-export path.
client_application_traffic_secret_Nandserver_application_traffic_secret_Nare stored inmbedtls_ssl_session.app_secrets; after processing a validNewSessionTicket,mbedtls_ssl_get_session()copies them into a caller-owned session intended for later resumption. A strengthened runtime probe shows that both traffic secrets remain non-zero in that exported object even after the original TLS connection has been freed. They disappear only when the caller explicitly frees the exported session.A follow-up key-recovery rerun on 2026-08-11 went beyond buffer-liveness and directly recreated both TLS 1.3 application traffic transforms from the exported client session after connection teardown. Using only the retained
app_secretscopy plus the negotiated ciphersuite, the probe successfully decrypted one captured client->server application record and one captured server->client application record from the original connection.Scope notes:
mbedtls_ssl_get_session()is client-only. The server has the same fields in its internal session, and the internal copy helper copies them, but this report no longer claims a public server export path.mbedtls_ssl_sessionobject.The second-pass runtime recheck completed a TLS 1.3 handshake, processed a server
NewSessionTicket, exported the client session with the public API, freed the original TLS connection, and then inspected whether the exported session still contained non-zero application traffic secrets.Standard Requirement
Official standard:
Section 7.1 gives the directly applicable normative rule:
Section 7.2 explains the valid reason for keeping the current generation while KeyUpdate is supported, and when an old generation should be deleted:
Appendix F describes the security consequence and the erasure condition for claiming forward secrecy:
The generated task snapshot called this a
MUST, but neither the quoted Appendix F sentence nor the more specific Section 7.1 rule hasMUSTstrength. The correct standards claim is therefore aSHOULD-level key-lifecycle issue plus a failure to meet Appendix F's erasure condition for the exported object's lifetime.TLS 1.3
client_application_traffic_secret_Nandserver_application_traffic_secret_Nare key-schedule outputs and are not among the Appendix F exceptions. Keeping a current generation inside a live connection can be necessary for KeyUpdate. Copying it into a resumption session that outlives the connection is not needed for traffic protection, KeyUpdate, ticket processing, session serialization, or resumption.Relevant Source Code
Positive cleanup paths:
implementions/mbedtls-development/library/ssl_tls13_keys.c:1467-1474destroys the internally generated key-share private key after raw agreement.implementions/mbedtls-development/library/ssl_tls13_keys.c:1500-1501zeroizes and frees the raw shared secret buffer.implementions/mbedtls-development/library/ssl_tls13_keys.c:1640-1644zeroizes handshake random bytes and transcript after deriving application keys.implementions/mbedtls-development/library/ssl_tls13_keys.c:1727-1729zeroizes the TLS 1.3 master-secret structure after deriving the resumption master secret.implementions/mbedtls-development/library/ssl_tls.c:4573-4596eventually zeroizes the wholembedtls_ssl_sessionobject during session free.Application traffic secrets are stored in session state:
implementions/mbedtls-development/library/ssl_tls13_keys.c:1531-1533The first application traffic secrets are then used to derive record-layer keys:
implementions/mbedtls-development/library/ssl_tls13_keys.c:1587-1591The session type embeds the full TLS 1.3 application-secret structure:
implementions/mbedtls-development/include/mbedtls/ssl.h:1085-1090implementions/mbedtls-development/include/mbedtls/ssl.h:1239-1240Session copy duplicates the whole structure:
implementions/mbedtls-development/library/ssl_tls.c:222-226mbedtls_ssl_get_session()calls that copy path:implementions/mbedtls-development/library/ssl_tls.c:3024-3051The API documentation defines the destination as an exported session for later resumption and serialization:
implementions/mbedtls-development/include/mbedtls/ssl.h:4770-4788The public export function is guarded by
MBEDTLS_SSL_CLI_Cand rejects non-client endpoints (ssl_tls.c:3023-3033). Thus only the client public export path is confirmed.There is no TLS 1.3 KeyUpdate processing path in this version. Post-handshake processing accepts
NewSessionTicketon clients and returnsMBEDTLS_ERR_SSL_UNEXPECTED_MESSAGEfor all other TLS 1.3 post-handshake messages:implementions/mbedtls-development/library/ssl_msg.c:5459-5485Implementation Behavior
For TLS 1.3, mbedTLS derives application secrets from the master secret and handshake transcript into
ssl->session_negotiate->app_secrets. It then derives application write keys and IVs fromclient_application_traffic_secret_Nandserver_application_traffic_secret_N.After transform population and the optional key-export callback, the code zeroizes temporary random/transcript buffers, but not the two application traffic secret fields. During TLS 1.3 handshake wrapup,
session_negotiatebecomes the activesession(implementions/mbedtls-development/library/ssl_tls13_generic.c:1250-1269).The codebase-wide references to the two fields are limited to derivation, initial traffic-key creation, debug/key-export callback output, structure declarations, and tests. They are not used for post-handshake record protection or KeyUpdate. Record protection uses keys in the active transform, while TLS 1.3 post-handshake dispatch has no KeyUpdate path.
On the client,
mbedtls_ssl_get_session()then copies the whole session withmemcpy, including both traffic secrets, into caller-owned resumption state. Freeing the originalmbedtls_ssl_contextdoes not affect that independent copy. Final cleanup does exist:mbedtls_ssl_session_free()zeroizes the copied object. The gap is therefore unnecessary retention from public export until caller cleanup, not total absence of cleanup.Additional boundary check: TLS 1.3 session serialization in
implementions/mbedtls-development/library/ssl_tls.c:3435-3573serializes ticket and resumption state, not the fullapp_secretsstructure. The concrete issue demonstrated here is the in-memory client API export, especially when that caller-owned object outlives the source connection.Inconsistency Reason
RFC9846 Section 7.1 says a secret
SHOULDbe erased once all values to be derived from it have been computed. Appendix F says that failing to erase ephemeral or connection-specific key-schedule material prevents the erasure condition underlying forward secrecy. mbedTLS satisfies this objective for the checked ephemeral private key, shared secret, handshake secrets, and master-secret cleanup paths.The implementation is incomplete for exported
client_application_traffic_secret_Nandserver_application_traffic_secret_N: both are connection-specific key-schedule outputs capable of deriving application record-layer keys; neither is needed by TLS 1.3 resumption; this version has no KeyUpdate consumer; yet the client public session-export API copies both into state designed to outlive the connection.This is a real, conditional security issue and a
SHOULD-level standards-alignment gap. It is conditional because applications that never callmbedtls_ssl_get_session(), or that serialize and immediately free the exported object, do not retain this extra post-connection copy. It is not correctly described as a strict RFCMUSTviolation.Runtime Evidence
The focused probe was strengthened to exercise the normal TLS 1.3 resumption-ticket workflow:
NewSessionTicket,mbedtls_ssl_get_session(),mbedtls_ssl_session,mbedtls_ssl_session_free()on the exported session.The probe was recompiled and rerun on 2026-08-10 for this second-pass review. It exercised the normal TLS 1.3 ticket-based resumption workflow, exported the client session through
mbedtls_ssl_get_session(), freed the original TLS connection, and then checked the independent exportedmbedtls_ssl_session.Result: exit code
0; the second-pass log includesrun_exit_code_observed_by_shell=0.The probe covers both forward-secret long-term-key cases called out by RFC9846. In the log,
1means the checked buffer was all-zero and0means non-zero bytes remained.cert_ephemeral-314880001psk_ephemeral-314880001Key second-pass log lines:
The strengthened result closes both ambiguities in the earlier probe: it observes the intended ticket-export workflow rather than exporting before a ticket exists, and it checks after connection teardown rather than only during an active connection. Handshake/master temporary material is zeroized. More importantly, the client public API exports both application traffic secrets, and those secrets remain non-zero after the source TLS connection has been destroyed. They are zeroized only by
mbedtls_ssl_session_free()on the caller-owned object.A follow-up rerun on 2026-08-11 used the same probe source with added recovery logic and logged to
manual_recheck_runtime/tls13_key_lifecycle_probe.key_recovery_20260811.log. That rerun captured one real application-data record in each direction before teardown, freed the original TLS connection, rebuilt the corresponding TLS 1.3 application transforms solely from the exportedmbedtls_ssl_session.app_secrets, and then decrypted both captured records successfully.cert_ephemeral11psk_ephemeral11Key recovery log lines:
Impact
The retained application traffic secrets are connection-specific TLS 1.3 key-schedule outputs. Copying them through
mbedtls_ssl_get_session()can extend their lifetime beyond connection teardown and place them in caller-managed session objects that applications retain for resumption.This is no longer only an inference from residual secret bytes: the 2026-08-11 rerun directly used the exported session object to reconstruct the old connection's client and server application traffic transforms and decrypt captured records after the original connection had already been freed. A serialized TLS 1.3 session is not affected because
mbedtls_ssl_session_save()omits these fields. Applications that promptly serialize and free the object also bound the exposure. Applications that retain the in-memory session inherit an unnecessary connection-traffic secret alongside the resumption material.Fix Direction
Split resumable session state from connection-specific application traffic state, or scrub connection-only fields in the client export path.
Keep only lifecycle-required values in exported session objects:
resumption_master_secretand resumption-derived ticket PSKs for resumption.exporter_master_secretonly while exporter APIs require it, or provide an explicit cleanup or disable path.client_application_traffic_secret_Norserver_application_traffic_secret_Nthroughmbedtls_ssl_get_session(). If changing the internal copy helper would break live-session uses, explicitly zero these two fields in the exported destination before returning.After successful application transform population and any configured key-export callback, zeroize
client_application_traffic_secret_Nandserver_application_traffic_secret_Nwhen they are no longer needed for record protection or KeyUpdate. If future KeyUpdate support requires retaining a traffic secret, keep it in connection transform state with a tightly scoped lifetime, not in exported resumable session state.