Skip to content

TLS 1.3 application traffic secrets survive public session export after connection teardown #10916

Description

@LiD0209

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:

  1. complete a TLS 1.3 handshake,
  2. have the server send a real NewSessionTicket,
  3. have the client process that ticket,
  4. call the public client API mbedtls_ssl_get_session(),
  5. free the original TLS connection,
  6. inspect the independent exported mbedtls_ssl_session,
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions