Cleanup pattern when a library both creates and wraps a PSA opaque key #816

Open
opened 2026-06-18 04:09:10 -05:00 by Harshal5 · 3 comments
Harshal5 commented 2026-06-18 04:09:10 -05:00 (Migrated from github.com)

The PSA Crypto API provides psa_destroy_key() (ends the key's lifetime; deletes persistent storage) and psa_purge_key() (releases the in-memory slot for persistent keys; no-op for volatile keys per psa_unregister_read).

TF-PSA-Crypto itself uses a type-based heuristic in mbedtls_pk_free(): if the wrapped key is MBEDTLS_PK_OPAQUE, the destroy is deliberately skipped: "the ownership of the priv_id key for opaque keys is external of the PK module. It's the user responsibility to clear it after use." That's clear when the opaque key is genuinely externally provided.

The ambiguous case is a library positioned as both the creator and the consumer of an opaque key. Consider a TLS library that internally calls psa_import_key() to wrap a custom opaque-driver context, then mbedtls_pk_wrap_psa() to plumb it into mbedTLS. mbedtls_pk_free() correctly leaves the destroy to the user, but in this case the library is the user. A subsequent psa_destroy_key() is needed.

This works as long as the library is the sole owner of every opaque key it wraps. Once the same library also gains a path that accepts externally-provided psa_key_id_t values (e.g., persistent keys supplied by the application, secure-element-backed keys with caller-managed lifetimes), the heuristic breaks: a psa_destroy_key() on the externally-provided id would silently delete the caller's key, while a psa_purge_key() would leak the slot for the still-existing volatile case.

Question:

Is per-context ownership tracking (a side flag distinguishing "this library created the opaque key" vs "this library is wrapping an externally-provided opaque key") the expected pattern for libraries in this position?

The PSA Crypto API provides `psa_destroy_key()` (ends the key's lifetime; deletes persistent storage) and `psa_purge_key()` (releases the in-memory slot for persistent keys; no-op for volatile keys per `psa_unregister_read`). TF-PSA-Crypto itself uses a type-based heuristic in `mbedtls_pk_free()`: if the wrapped key is `MBEDTLS_PK_OPAQUE`, the destroy is deliberately skipped: "the ownership of the priv_id key for opaque keys is external of the PK module. It's the user responsibility to clear it after use." That's clear when the opaque key is genuinely externally provided. The ambiguous case is a library positioned as both the creator and the consumer of an opaque key. Consider a TLS library that internally calls `psa_import_key()` to wrap a custom opaque-driver context, then `mbedtls_pk_wrap_psa()` to plumb it into `mbedTLS`. `mbedtls_pk_free()` correctly leaves the destroy to the user, but in this case the library is the user. A subsequent `psa_destroy_key()` is needed. This works as long as the library is the sole owner of every opaque key it wraps. Once the same library also gains a path that accepts externally-provided `psa_key_id_t` values (e.g., persistent keys supplied by the application, secure-element-backed keys with caller-managed lifetimes), the heuristic breaks: a `psa_destroy_key()` on the externally-provided id would silently delete the caller's key, while a `psa_purge_key()` would leak the slot for the still-existing volatile case. ### Question: Is per-context ownership tracking (a side flag distinguishing "this library created the opaque key" vs "this library is wrapping an externally-provided opaque key") the expected pattern for libraries in this position?
Harshal5 commented 2026-06-18 04:09:43 -05:00 (Migrated from github.com)

@gilles-peskine-arm Could you please help clarify this?

@gilles-peskine-arm Could you please help clarify this?
gilles-peskine-arm commented 2026-06-19 13:02:26 -05:00 (Migrated from github.com)

The intent of the interface is to expose two functions for using a PSA key through PK (and thus X.509, TLS, or anything similar):

  • mbedtls_pk_copy_from_psa() to create a new PSA key owned by the PK context. The original PSA key is only borrowed for the duration of the call to mbedtls_pk_copy_from_psa().
  • mbedtls_pk_wrap_psa() to create only a PK context which directly references the original PSA key. The PK context is a reader of the PSA key. It's up to the caller to ensure that the PSA key is not destroyed while it has readers, and is destroyed once it has no readers (PK contexts or otherwise).

This API does leave a gap: there's no way to transfer ownership of an existing PSA key to a PK context. This is a reasonable thing to want, but so far we don't offer it, for several reasons:

  • There was no such interface in Mbed TLS 3.6, and the main intent of the PK module is to keep supporting applications that were already using PK before PSA came along.
  • It's functionally equivalent to mbedtls_pk_copy_from_psa() followed by destroying the original PSA key. Granted, it uses a bit more resources, and it doesn't work in a few edge cases (key without the copy permission).
  • It would make the interface more complex, with the risk of more mistakes both in our own code and in application (or third-party library) code, of either destroying a PSA key that's still in use or forgetting to free a PSA key that no PK context owns.

But if the limitations are a serious problem, we could add this function. mbedtls_pk_seize_psa_key()?

The intent of the interface is to expose two functions for using a PSA key through PK (and thus X.509, TLS, or anything similar): * `mbedtls_pk_copy_from_psa()` to create a new PSA key owned by the PK context. The original PSA key is only borrowed for the duration of the call to `mbedtls_pk_copy_from_psa()`. * `mbedtls_pk_wrap_psa()` to create only a PK context which directly references the original PSA key. The PK context is a reader of the PSA key. It's up to the caller to ensure that the PSA key is not destroyed while it has readers, and is destroyed once it has no readers (PK contexts or otherwise). This API does leave a gap: there's no way to transfer ownership of an existing PSA key to a PK context. This is a reasonable thing to want, but so far we don't offer it, for several reasons: * There was no such interface in Mbed TLS 3.6, and the main intent of the PK module is to keep supporting applications that were already using PK before PSA came along. * It's functionally equivalent to `mbedtls_pk_copy_from_psa()` followed by destroying the original PSA key. Granted, it uses a bit more resources, and it doesn't work in a few edge cases (key without the copy permission). * It would make the interface more complex, with the risk of more mistakes both in our own code and in application (or third-party library) code, of either destroying a PSA key that's still in use or forgetting to free a PSA key that no PK context owns. But if the limitations are a serious problem, we could add this function. `mbedtls_pk_seize_psa_key()`?
Harshal5 commented 2026-06-22 23:48:15 -05:00 (Migrated from github.com)

cc: @Ashish285

cc: @Ashish285
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-Mbed-TLS/TF-PSA-Crypto#816
No description provided.