Skip to content

TLS 1.3 transform setup can leave an imported traffic key undisposed on failure #10917

Description

@LiD0209

TLS 1.3 transform setup can leave an imported traffic key undisposed on failure

Summary

This is a real issue, but the scope is narrower than a broad TLS 1.3 key-erasure failure. Normal mbedTLS session and transform free paths do erase or destroy the checked key material. The confirmed problem is the partial transform setup path: if mbedtls_ssl_tls13_populate_transform() imports the encryption PSA key successfully and then fails while importing the decryption PSA key, the already imported encryption key is not destroyed. The callers then free the local transform allocation directly, bypassing mbedtls_ssl_transform_free().

Standard Check

RFC 9846 supports this as a key-lifetime/security cleanup issue, but not as an uppercase MUST violation.

Official standard references:

The precise requirement is therefore: connection-specific TLS 1.3 traffic key material should not have its lifetime extended after it is no longer usable. After transform setup fails, the partially imported PSA traffic key has no valid connection owner and should be destroyed.

Implementation Check

In mbedtls_ssl_tls13_populate_transform(), the encryption key is imported first:

/* implementions/mbedtls-development/library/ssl_tls13_keys.c:1020 */
if ((status = psa_import_key(&attributes,
                             key_enc,
                             PSA_BITS_TO_BYTES(key_bits),
                             &transform->psa_key_enc)) != PSA_SUCCESS) {
    return PSA_TO_MBEDTLS_ERR(status);
}

The decrypt key is imported second. If this import fails, the function returns without destroying transform->psa_key_enc:

/* implementions/mbedtls-development/library/ssl_tls13_keys.c:1031 */
if ((status = psa_import_key(&attributes,
                             key_dec,
                             PSA_BITS_TO_BYTES(key_bits),
                             &transform->psa_key_dec)) != PSA_SUCCESS) {
    return PSA_TO_MBEDTLS_ERR(status);
}

The TLS 1.3 callers zeroize the temporary traffic_keys buffer, but directly free the partially populated transform on error:

/* implementions/mbedtls-development/library/ssl_tls13_keys.c:1218 */
mbedtls_platform_zeroize(&traffic_keys, sizeof(traffic_keys));
if (ret != 0) {
    mbedtls_free(transform_earlydata);
}
/* implementions/mbedtls-development/library/ssl_tls13_keys.c:1689 */
mbedtls_platform_zeroize(&traffic_keys, sizeof(traffic_keys));
if (ret != 0) {
    mbedtls_free(transform_handshake);
}
/* implementions/mbedtls-development/library/ssl_tls13_keys.c:1783 */
mbedtls_platform_zeroize(&traffic_keys, sizeof(traffic_keys));
if (ret != 0) {
    mbedtls_free(transform_application);
}

The normal transform destructor would destroy both PSA keys and zeroize the transform:

/* implementions/mbedtls-development/library/ssl_msg.c:6090 */
psa_destroy_key(transform->psa_key_enc);
psa_destroy_key(transform->psa_key_dec);
mbedtls_platform_zeroize(transform, sizeof(mbedtls_ssl_transform));

The PSA API documentation confirms that psa_destroy_key() is the operation that destroys key material and frees associated key resources (implementions/mbedtls-development/tf-psa-crypto/include/psa/crypto.h:533-540). The PSA transition guide also states that a key created with psa_import_key() should finally be destroyed and consumes a key store entry until destruction or application exit (implementions/mbedtls-development/tf-psa-crypto/docs/psa-transition.md:103-111).

Runtime Evidence

The real-key persistence probe was rebuilt and run during the recheck. It wraps psa_import_key() so that the first call goes to the real PSA implementation and the second call fails with PSA_ERROR_INSUFFICIENT_MEMORY. It then frees the transform as the callers do and checks whether the first PSA key still exists.

Run result:

compile_exit=0
ret=-141 import_call_count=2 destroy_before_cleanup=0 enc_key_id=1073741824 lookup_after_return=0 manual_cleanup=0
probe_exit=0

Interpretation:

  • compile_exit=0: the focused probe rebuilt successfully.
  • import_call_count=2: the transform path attempted both PSA imports.
  • ret=-141: the injected second import failure propagated out of mbedtls_ssl_tls13_populate_transform().
  • destroy_before_cleanup=0: no psa_destroy_key() call occurred before the caller-style direct transform free.
  • lookup_after_return=0: psa_get_key_attributes(saved_key) returned PSA_SUCCESS, proving the first real PSA key still existed after the transform was directly freed.
  • manual_cleanup=0: the probe's final manual psa_destroy_key(saved_key) succeeded, confirming the residual object was still a live PSA key.
  • probe_exit=0: the probe considered the issue reproduced.

Control run:

app_secrets_zero_after_session_free=1 sizeof_app_secrets=256
resumption_key_zero_after_session_free=1
transform_zero_after_free=1 sizeof_transform=240
control_exit=0

The control confirms that normal explicit session and transform free paths erase the checked key material. The confirmed issue is limited to the partial TLS 1.3 transform population failure path.

Impact

The affected path requires a failure during the second PSA key import after the first import has succeeded, for example due to PSA allocation or resource exhaustion. In that case, a TLS 1.3 traffic key imported into PSA can remain undisposed after handshake or transform setup failure. Depending on the PSA backend, this can leak a transient key slot and extend the lifetime of connection-specific traffic key material beyond the failed connection setup.

Practical severity is likely low to medium because the path requires an internal resource/failure condition, but it is a real cleanup defect for sensitive key material.

Fix Direction

Prefer central cleanup inside mbedtls_ssl_tls13_populate_transform():

  • Track successful PSA imports.
  • On any later failure, destroy already imported PSA keys and zeroize the partial transform, or call a cleanup helper that is safe for partially initialized transforms.

Changing each TLS 1.3 caller to call mbedtls_ssl_transform_free() before mbedtls_free() would also address current callers, but central cleanup is safer for future call sites.

Metadata

Metadata

Assignees

Type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions