From ae5a50e6ffb5fdd5ebff217e1b493e44a8e40cbc Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 5 Nov 2026 09:15:13 -0800 Subject: [PATCH] wrap: flush only the transient handles the TPM reports as loaded --- .gitignore | 2 + examples/pqc/pqc_ctrl.c | 42 +---- src/tpm2_wrap.c | 57 ++++++- tests/include.am | 8 + tests/transient_unload_tests.c | 282 +++++++++++++++++++++++++++++++++ tests/unit_tests.c | 31 ++++ wolftpm/tpm2_wrap.h | 1 + 7 files changed, 384 insertions(+), 39 deletions(-) create mode 100644 tests/transient_unload_tests.c diff --git a/.gitignore b/.gitignore index 71a6162ac..8760b25a6 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,8 @@ tests/packet_auth.test tests/packet_auth.log tests/signature_padding.test tests/signature_padding.log +tests/transient_unload.test +tests/transient_unload.log tests/spdm_config.test tests/spdm_config.log tests/spdm_ctrl.test diff --git a/examples/pqc/pqc_ctrl.c b/examples/pqc/pqc_ctrl.c index 897cb089e..b4c53903a 100644 --- a/examples/pqc/pqc_ctrl.c +++ b/examples/pqc/pqc_ctrl.c @@ -120,40 +120,6 @@ static const char* mlkemName(TPMI_MLKEM_PARAMETER_SET ps) "768"; } -/* Reclaim the transient object table by flushing exactly the handles the TPM - * reports as loaded. Parts with a small object table (e.g. SealSQ QVault TPM) - * hit TPM_RC_OBJECT_MEMORY when a key plus a sign/verify sequence object are - * live at once. wolfTPM2_UnloadHandles_AllTransient() only covers - * MAX_HANDLE_NUM (3) fixed handles, so query the actual loaded set instead. */ -static int pqc_flush_transient(WOLFTPM2_DEV* dev) -{ - int rc; - word32 i; - GetCapability_In in; - GetCapability_Out out; - FlushContext_In flushCtx; - - (void)dev; /* operates on the active TPM context */ - XMEMSET(&in, 0, sizeof(in)); - XMEMSET(&out, 0, sizeof(out)); - XMEMSET(&flushCtx, 0, sizeof(flushCtx)); - in.capability = TPM_CAP_HANDLES; - in.property = TRANSIENT_FIRST; - in.propertyCount = MAX_CAP_HANDLES; - rc = TPM2_GetCapability(&in, &out); - if (rc != TPM_RC_SUCCESS) { - return rc; - } - if (out.capabilityData.capability != TPM_CAP_HANDLES) { - return TPM_RC_FAILURE; - } - for (i = 0; i < out.capabilityData.data.handles.count; i++) { - flushCtx.flushHandle = out.capabilityData.data.handles.handle[i]; - (void)TPM2_FlushContext(&flushCtx); - } - return TPM_RC_SUCCESS; -} - static int do_caps(WOLFTPM2_DEV* dev) { int rc; @@ -401,7 +367,7 @@ static int do_mldsa(WOLFTPM2_DEV* dev, TPMI_MLDSA_PARAMETER_SET ps) XMEMSET(&validation, 0, sizeof(validation)); XMEMSET(&flushCtx, 0, sizeof(flushCtx)); - (void)pqc_flush_transient(dev); + (void)wolfTPM2_UnloadHandles_AllTransient(dev); sig = (byte*)XMALLOC(MAX_MLDSA_SIG_SIZE, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (sig == NULL) @@ -495,7 +461,7 @@ static int do_hash_mldsa(WOLFTPM2_DEV* dev, TPMI_MLDSA_PARAMETER_SET ps) #endif digestSz = TPM2_GetHashDigestSize(preHash); - (void)pqc_flush_transient(dev); + (void)wolfTPM2_UnloadHandles_AllTransient(dev); sig = (byte*)XMALLOC(MAX_MLDSA_SIG_SIZE, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (sig == NULL) @@ -571,7 +537,7 @@ static int do_mlkem(WOLFTPM2_DEV* dev, TPMI_MLKEM_PARAMETER_SET ps) XMEMSET(ss1, 0, sizeof(ss1)); XMEMSET(ss2, 0, sizeof(ss2)); - (void)pqc_flush_transient(dev); + (void)wolfTPM2_UnloadHandles_AllTransient(dev); ct = (byte*)XMALLOC(MAX_MLKEM_CT_SIZE, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (ct == NULL) @@ -721,7 +687,7 @@ static int handleOpt(WOLFTPM2_DEV* dev, const char* arg, int validateOnly) if (validateOnly) { return 0; } - rc = pqc_flush_transient(dev); + rc = wolfTPM2_UnloadHandles_AllTransient(dev); if (rc != TPM_RC_SUCCESS) { printf("FAIL flush 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc)); return rc; diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 097fc830a..7a5832549 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -9450,7 +9450,62 @@ int wolfTPM2_UnloadHandles(WOLFTPM2_DEV* dev, word32 handleStart, int wolfTPM2_UnloadHandles_AllTransient(WOLFTPM2_DEV* dev) { - return wolfTPM2_UnloadHandles(dev, TRANSIENT_FIRST, MAX_HANDLE_NUM); + int rc; + int flushRc; + int firstFlushRc = TPM_RC_SUCCESS; + word32 i; + TPM_HANDLE next = TRANSIENT_FIRST; + GetCapability_In in; + GetCapability_Out out; + TPML_HANDLE* handles = &out.capabilityData.data.handles; + WOLFTPM2_HANDLE handle; + + if (dev == NULL) { + return BAD_FUNC_ARG; + } + + /* flush only the handles the TPM reports as loaded */ + XMEMSET(&handle, 0, sizeof(handle)); + wolfTPM2_CopyAuth(&handle.auth, &dev->session[0].auth); + for (;;) { + XMEMSET(&in, 0, sizeof(in)); + in.capability = TPM_CAP_HANDLES; + in.property = next; + in.propertyCount = MAX_CAP_HANDLES; + rc = TPM2_GetCapability(&in, &out); + if (rc == TPM_RC_SUCCESS && + out.capabilityData.capability != TPM_CAP_HANDLES) { + rc = TPM_RC_FAILURE; + } + if (rc != TPM_RC_SUCCESS) { + break; + } + for (i = 0; i < handles->count; i++) { + if (handles->handle[i] < next || + (handles->handle[i] & HR_RANGE_MASK) != HR_TRANSIENT) { + break; + } + next = handles->handle[i] + 1; + handle.hndl = handles->handle[i]; + flushRc = wolfTPM2_UnloadHandle(dev, &handle); + if (flushRc != TPM_RC_SUCCESS && firstFlushRc == TPM_RC_SUCCESS) { + firstFlushRc = flushRc; + } + } + /* continue only after a non-empty page of ascending transient + * handles that reports more data */ + if (!out.moreData || handles->count == 0 || i < handles->count) { + break; + } + } + TPM2_ForceZero(&handle, sizeof(handle)); + + /* The fixed range ignores individual flush results, so it cannot confirm + * anything was unloaded: try it, but report that discovery failed. */ + if (rc != TPM_RC_SUCCESS && next == TRANSIENT_FIRST) { + (void)wolfTPM2_UnloadHandles(dev, TRANSIENT_FIRST, MAX_HANDLE_NUM); + } + return (rc != TPM_RC_SUCCESS) ? rc : firstFlushRc; } int wolfTPM2_ChangeHierarchyAuth(WOLFTPM2_DEV* dev, WOLFTPM2_SESSION* session, diff --git a/tests/include.am b/tests/include.am index ce3aa5df4..f67ff5eb6 100644 --- a/tests/include.am +++ b/tests/include.am @@ -136,5 +136,13 @@ signature_padding_support_objects = \ tests_signature_padding_test_LDADD = $(signature_padding_support_objects) \ $(src_libwolftpm_la_LIBADD) $(LIB_STATIC_ADD) tests_signature_padding_test_DEPENDENCIES = src/libwolftpm.la +# Scripted TPM_CAP_HANDLES pages, which a simulator cannot produce on demand. +check_PROGRAMS += tests/transient_unload.test +noinst_PROGRAMS += tests/transient_unload.test +tests_transient_unload_test_SOURCES = tests/transient_unload_tests.c +tests_transient_unload_test_CFLAGS = $(AM_CFLAGS) +tests_transient_unload_test_LDADD = $(signature_padding_support_objects) \ + $(src_libwolftpm_la_LIBADD) $(LIB_STATIC_ADD) +tests_transient_unload_test_DEPENDENCIES = src/libwolftpm.la endif endif diff --git a/tests/transient_unload_tests.c b/tests/transient_unload_tests.c new file mode 100644 index 000000000..1ef96729b --- /dev/null +++ b/tests/transient_unload_tests.c @@ -0,0 +1,282 @@ +/* transient_unload_tests.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfTPM. + * + * wolfTPM is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfTPM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include +#include + +#include +#include + +#define Fail(description, result) do { \ + printf("\nERROR - %s line %d failed with:", __FILE__, __LINE__); \ + printf("\n expected: "); printf description; \ + printf("\n result: "); printf result; printf("\n\n"); \ + fflush(stdout); \ + abort(); \ +} while (0) +#define Assert(test, description, result) if (!(test)) Fail(description, result) +#define AssertInt(x, y, op, er) do { \ + int _x = (int)(x); \ + int _y = (int)(y); \ + Assert(_x op _y, ("%s " #op " %s", #x, #y), \ + ("%d(0x%x) " #er " %d(0x%x)", _x, _x, _y, _y)); \ +} while (0) +#define AssertIntEQ(x, y) AssertInt(x, y, ==, !=) + +#define TEST_MAX_PAGES 3 +#define TEST_PAGE_HANDLES 4 +#define TEST_MAX_FLUSHES (2 * MAX_HANDLE_NUM) + +/* Scripted TPM_CAP_HANDLES pages and a record of what the wrapper sent */ +static struct { + TPM_HANDLE page[TEST_MAX_PAGES][TEST_PAGE_HANDLES]; + int pageCount[TEST_MAX_PAGES]; + byte pageMore[TEST_MAX_PAGES]; + TPM_RC pageRc[TEST_MAX_PAGES]; + int pages; + TPM_HANDLE queried[TEST_MAX_PAGES]; + TPM_HANDLE flushed[TEST_MAX_FLUSHES]; + int flushes; + TPM_HANDLE flushFail; +} script; + +/* Exercise the real wrapper, command builders and response parser without a + * TPM or socket. */ +#if defined(WOLFTPM_LINUX_DEV) + #define TPM2_LINUX_SendCommand test_unload_send +#elif defined(WOLFTPM_LINUX_DEV_AUTODETECT) + #define TPM2_LINUX_AUTODETECT_SendCommand test_unload_send +#elif defined(WOLFTPM_SWTPM) + #define TPM2_SWTPM_SendCommand test_unload_send +#elif defined(WOLFTPM_WINAPI) + #define TPM2_WinApi_SendCommand test_unload_send +#else + #define TPM2_TIS_SendCommand test_unload_send +#endif +#include "../src/tpm2.c" + +static UINT32 test_get_u32(const byte* buf) +{ + return ((UINT32)buf[0] << 24) | ((UINT32)buf[1] << 16) | + ((UINT32)buf[2] << 8) | buf[3]; +} + +int test_unload_send(TPM2_CTX* ctx, TPM2_Packet* packet) +{ + TPM2_Packet rsp; + TPM_RC rc = TPM_RC_SUCCESS; + UINT32 code = test_get_u32(packet->buf + 6); + /* the response is built over the command, so read its arguments first */ + UINT32 arg0 = test_get_u32(packet->buf + TPM2_HEADER_SIZE); + UINT32 arg1 = test_get_u32(packet->buf + TPM2_HEADER_SIZE + 4); + int page = script.pages; + int i; + + TPM2_Packet_Init(ctx, &rsp); + if (code == TPM_CC_FlushContext) { + AssertIntEQ(script.flushes < TEST_MAX_FLUSHES, 1); + script.flushed[script.flushes++] = arg0; + if (arg0 == script.flushFail) { + rc = TPM_RC_FAILURE; + } + } + else { + AssertIntEQ(code, TPM_CC_GetCapability); + AssertIntEQ(arg0, TPM_CAP_HANDLES); + AssertIntEQ(page < TEST_MAX_PAGES, 1); + script.queried[page] = arg1; + script.pages++; + rc = script.pageRc[page]; + if (rc == TPM_RC_SUCCESS) { + TPM2_Packet_AppendU8(&rsp, script.pageMore[page]); + TPM2_Packet_AppendU32(&rsp, TPM_CAP_HANDLES); + TPM2_Packet_AppendU32(&rsp, (UINT32)script.pageCount[page]); + for (i = 0; i < script.pageCount[page]; i++) { + TPM2_Packet_AppendU32(&rsp, script.page[page][i]); + } + } + } + TPM2_Packet_Finalize(&rsp, TPM_ST_NO_SESSIONS, rc); + packet->size = rsp.pos; + return TPM_RC_SUCCESS; +} + +#if !defined(WOLFTPM_LINUX_DEV) && !defined(WOLFTPM_SWTPM) && \ + !defined(WOLFTPM_WINAPI) +#ifdef WOLFTPM_ADV_IO +static int test_unload_io(TPM2_CTX* ctx, INT32 read, UINT32 addr, + BYTE* buf, UINT16 sz, void* user) +{ + (void)read; + (void)addr; + (void)buf; +#else +static int test_unload_io(TPM2_CTX* ctx, const BYTE* tx, BYTE* rx, + UINT16 sz, void* user) +{ + (void)tx; + (void)rx; +#endif + (void)ctx; + (void)sz; + (void)user; + return TPM_RC_FAILURE; +} +#endif /* callback transports */ + +static void test_setup(WOLFTPM2_DEV* dev) +{ + int rc; + + XMEMSET(dev, 0, sizeof(*dev)); + XMEMSET(&script, 0, sizeof(script)); +#if defined(WOLFTPM_LINUX_DEV) || defined(WOLFTPM_SWTPM) || \ + defined(WOLFTPM_WINAPI) + rc = TPM2_Init_minimal(&dev->ctx); +#else + rc = TPM2_Init_ex(&dev->ctx, test_unload_io, NULL, 0); +#endif + AssertIntEQ(rc, TPM_RC_SUCCESS); +} + +/* A partial page with moreData continues from the handle after its last one, + * so handles on the next page are flushed as well */ +static void test_two_pages(void) +{ + WOLFTPM2_DEV dev; + + test_setup(&dev); + script.pageCount[0] = 2; + script.page[0][0] = TRANSIENT_FIRST; + script.page[0][1] = TRANSIENT_FIRST + 1; + script.pageMore[0] = 1; + script.pageCount[1] = 1; + script.page[1][0] = TRANSIENT_FIRST + 4; + + AssertIntEQ(wolfTPM2_UnloadHandles_AllTransient(&dev), TPM_RC_SUCCESS); + AssertIntEQ(script.pages, 2); + AssertIntEQ(script.queried[0], TRANSIENT_FIRST); + AssertIntEQ(script.queried[1], TRANSIENT_FIRST + 2); + AssertIntEQ(script.flushes, 3); + AssertIntEQ(script.flushed[0], TRANSIENT_FIRST); + AssertIntEQ(script.flushed[1], TRANSIENT_FIRST + 1); + AssertIntEQ(script.flushed[2], TRANSIENT_FIRST + 4); + TPM2_Cleanup(&dev.ctx); +} + +/* Nothing loaded: no flush is attempted */ +static void test_none_loaded(void) +{ + WOLFTPM2_DEV dev; + + test_setup(&dev); + AssertIntEQ(wolfTPM2_UnloadHandles_AllTransient(&dev), TPM_RC_SUCCESS); + AssertIntEQ(script.pages, 1); + AssertIntEQ(script.flushes, 0); + TPM2_Cleanup(&dev.ctx); +} + +/* When discovery fails the fixed range is still tried, but nothing confirms it + * worked, so the failure reaches the caller */ +static void test_query_failure(void) +{ + WOLFTPM2_DEV dev; + + test_setup(&dev); + script.pageRc[0] = TPM_RC_FAILURE; + AssertIntEQ(wolfTPM2_UnloadHandles_AllTransient(&dev), TPM_RC_FAILURE); + AssertIntEQ(script.flushes, MAX_HANDLE_NUM); + AssertIntEQ(script.flushed[0], TRANSIENT_FIRST); + TPM2_Cleanup(&dev.ctx); +} + +/* A later page failing is reported without restarting the fixed range */ +static void test_second_page_failure(void) +{ + WOLFTPM2_DEV dev; + + test_setup(&dev); + script.pageCount[0] = 1; + script.page[0][0] = TRANSIENT_FIRST; + script.pageMore[0] = 1; + script.pageRc[1] = TPM_RC_FAILURE; + AssertIntEQ(wolfTPM2_UnloadHandles_AllTransient(&dev), TPM_RC_FAILURE); + AssertIntEQ(script.flushes, 1); + TPM2_Cleanup(&dev.ctx); +} + +/* A failed flush is reported, and the remaining handles are still flushed */ +static void test_flush_failure(void) +{ + WOLFTPM2_DEV dev; + + test_setup(&dev); + script.pageCount[0] = 2; + script.page[0][0] = TRANSIENT_FIRST; + script.page[0][1] = TRANSIENT_FIRST + 1; + script.flushFail = TRANSIENT_FIRST; + AssertIntEQ(wolfTPM2_UnloadHandles_AllTransient(&dev), TPM_RC_FAILURE); + AssertIntEQ(script.flushes, 2); + TPM2_Cleanup(&dev.ctx); +} + +/* A handle outside the transient range or out of order ends the walk unflushed + * and does not follow moreData */ +static void test_bad_handles(void) +{ + WOLFTPM2_DEV dev; + + test_setup(&dev); + script.pageCount[0] = 2; + script.page[0][0] = TRANSIENT_FIRST; + script.page[0][1] = HMAC_SESSION_FIRST; + script.pageMore[0] = 1; + AssertIntEQ(wolfTPM2_UnloadHandles_AllTransient(&dev), TPM_RC_SUCCESS); + AssertIntEQ(script.pages, 1); + AssertIntEQ(script.flushes, 1); + TPM2_Cleanup(&dev.ctx); + + test_setup(&dev); + script.pageCount[0] = 2; + script.page[0][0] = TRANSIENT_FIRST + 1; + script.page[0][1] = TRANSIENT_FIRST; + script.pageMore[0] = 1; + AssertIntEQ(wolfTPM2_UnloadHandles_AllTransient(&dev), TPM_RC_SUCCESS); + AssertIntEQ(script.pages, 1); + AssertIntEQ(script.flushes, 1); + TPM2_Cleanup(&dev.ctx); +} + +int main(void) +{ + test_two_pages(); + test_none_loaded(); + test_query_failure(); + test_second_page_failure(); + test_flush_failure(); + test_bad_handles(); + printf("Transient unload tests: passed\n"); + return 0; +} diff --git a/tests/unit_tests.c b/tests/unit_tests.c index b0a50cc3d..e2543426c 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1866,6 +1866,36 @@ static void test_wolfTPM2_GetRandom(void) rc == 0 ? "Passed" : "Failed"); } +static void test_wolfTPM2_UnloadHandles_AllTransient(void) +{ + int rc; + WOLFTPM2_DEV dev; + WOLFTPM2_KEY srk; + int eccSupported = 0; + + rc = wolfTPM2_Init(&dev, TPM2_IoCb, NULL); + AssertIntEQ(rc, 0); + + rc = wolfTPM2_UnloadHandles_AllTransient(NULL); + AssertIntEQ(rc, BAD_FUNC_ARG); + + AssertIntEQ(wolfTPM2_IsAlgSupported(&dev, TPM_ALG_ECC, &eccSupported), 0); + XMEMSET(&srk, 0, sizeof(srk)); + rc = wolfTPM2_CreateSRK(&dev, &srk, + eccSupported ? TPM_ALG_ECC : TPM_ALG_RSA, NULL, 0); + AssertIntEQ(rc, 0); + AssertIntGE(wolfTPM2_GetHandles(TRANSIENT_FIRST, NULL), 1); + + rc = wolfTPM2_UnloadHandles_AllTransient(&dev); + AssertIntEQ(rc, 0); + AssertIntEQ(wolfTPM2_GetHandles(TRANSIENT_FIRST, NULL), 0); + + wolfTPM2_Cleanup(&dev); + + printf("Test TPM Wrapper: %-40s %s\n", "Unload All Transient:", + rc == 0 ? "Passed" : "Failed"); +} + static void test_wolfTPM2_HashFinish_BufferTooSmall(void) { #ifndef WOLFTPM2_NO_WOLFCRYPT @@ -11847,6 +11877,7 @@ int unit_tests(int argc, char *argv[]) test_wolfTPM2_OpenExisting(); test_wolfTPM2_GetCapabilities(); test_wolfTPM2_GetRandom(); + test_wolfTPM2_UnloadHandles_AllTransient(); test_wolfTPM2_DictionaryAttack(); test_wolfTPM2_HashFinish_BufferTooSmall(); test_TPM2_PCRSel(); diff --git a/wolftpm/tpm2_wrap.h b/wolftpm/tpm2_wrap.h index 8b19603ec..fc6d7989a 100644 --- a/wolftpm/tpm2_wrap.h +++ b/wolftpm/tpm2_wrap.h @@ -4042,6 +4042,7 @@ WOLFTPM_API int wolfTPM2_UnloadHandles(WOLFTPM2_DEV* dev, word32 handleStart, /*! \ingroup wolfTPM2_Wrappers \brief One-shot API to unload all transient TPM handles + \note Flushes the transient handles the TPM reports as loaded, following TPM_CAP_HANDLES continuation pages, or the first MAX_HANDLE_NUM if the first TPM_CAP_HANDLES query fails. A failed query or flush is returned; a failed flush does not stop the remaining handles from being flushed \note If there are Primary Keys as transient objects, they need to be recreated before TPM keys can be used \return TPM_RC_SUCCESS: successful