diff --git a/ChangeLog.md b/ChangeLog.md index ec1c2483..0f4b1bd5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -27,6 +27,14 @@ client wrappers, DA/noDA/lockout/self-heal/persistence unit tests, an `examples/management/da_check` end-to-end example, and the `tests/fwtpm_da_retry.sh` CI harness. +* Added optional transparent `TPM_RC_RETRY` handling so commands can be + automatically resubmitted when the TPM reports it is momentarily busy (for + example persisting the daUsed flag on first auth use of an externally + provisioned non-noDA AIK/SUDI key). Disabled by default to preserve the raw + TPM response code; opt in via `TPM2_SetCommandRetries` at runtime or + `-DWOLFTPM_MAX_RETRIES=N` at build time. Define `WOLFTPM_NO_RETRY` to compile + the handling out entirely. wolfTPM's own key templates set `noDA` and never + trigger it. ## wolfTPM Release 4.0.0 (Apr 22, 2026) diff --git a/README.md b/README.md index bbe164bc..b155c7e0 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,8 @@ WOLFTPM2_USE_SW_ECDHE Disables use of TPM for ECC ephemeral key generation and WOLFTPM2_ECC_DEFAULT_CURVE Default ECC curve for wrapper key templates that request P256 (SRK/AIK/general ECC). Defaults to TPM_ECC_NIST_P256, or the smallest enabled curve meeting ECC_MIN_KEY_SZ. Override e.g. -DWOLFTPM2_ECC_DEFAULT_CURVE=TPM_ECC_NIST_P384. TLS_BENCH_MODE Enables TLS benchmarking mode. NO_TPM_BENCH Disables the TPM benchmarking example. +WOLFTPM_MAX_RETRIES Default number of times a command is transparently resubmitted when the TPM returns TPM_RC_RETRY (momentarily busy, e.g. persisting the daUsed flag on first auth use of an externally provisioned non-noDA AIK/SUDI key). Disabled by default (0); opt in with TPM2_SetCommandRetries() at runtime or -DWOLFTPM_MAX_RETRIES=N at build time. wolfTPM's own key templates set noDA and never trigger it. +WOLFTPM_NO_RETRY Compiles out the TPM_RC_RETRY auto-resubmit handling entirely; TPM_RC_RETRY is returned to the caller for manual handling. ``` Note: For the I2C support on Raspberry Pi you may need to enable I2C. Here are the steps: diff --git a/src/tpm2.c b/src/tpm2.c index aa95dbf0..610acaa5 100644 --- a/src/tpm2.c +++ b/src/tpm2.c @@ -452,7 +452,13 @@ static TPM_RC TPM2_SPDM_SendCommand(TPM2_CTX* ctx, TPM2_Packet* packet) packet->buf, packet->pos, tpmResp, &tpmRespSz); if (rc != 0) { TPM2_ForceZero(tpmResp, sizeof(tpmResp)); - return rc; + #ifdef DEBUG_WOLFTPM + printf("SPDM secured exchange failed: %d\n", rc); + #endif + /* SPDM is active: never downgrade to cleartext. Map the (negative) + * transport error to a positive TPM RC so the caller treats it as a + * hard failure instead of "SPDM not active". */ + return TPM_RC_FAILURE; } if (tpmRespSz > MAX_RESPONSE_SIZE) { @@ -467,6 +473,87 @@ static TPM_RC TPM2_SPDM_SendCommand(TPM2_CTX* ctx, TPM2_Packet* packet) } #endif /* WOLFTPM_SPDM */ +/* Send the finalized command in packet over the SPDM secured channel when a + * session is active, otherwise over the raw transport. Once SPDM is active an + * exchange failure is returned as-is and never falls back to a cleartext send, + * so a forced SPDM failure cannot downgrade the link. */ +static TPM_RC TPM2_DispatchCommand(TPM2_CTX* ctx, TPM2_Packet* packet) +{ + TPM_RC rc; + +#ifdef WOLFTPM_SPDM + rc = TPM2_SPDM_SendCommand(ctx, packet); + if (rc >= 0) + return rc; /* SPDM active: success or hard failure, no cleartext */ + /* rc < 0: SPDM not active, use normal transport */ +#endif + rc = (TPM_RC)INTERNAL_SEND_COMMAND(ctx, packet); + + return rc; +} + +#ifdef WOLFTPM_NO_RETRY +/* Submit the finalized command in packet (length cmdSz) and parse the + * response, returning the TPM response code. */ +static TPM_RC TPM2_TransmitCommand(TPM2_CTX* ctx, TPM2_Packet* packet, + UINT32 cmdSz) +{ + TPM_RC rc; + + /* send command requires packet->pos to be the total command length */ + packet->pos = cmdSz; + + rc = TPM2_DispatchCommand(ctx, packet); + if (rc != 0) + return rc; /* transport or SPDM error */ + + /* parse response header and extract the TPM response code */ + rc = TPM2_Packet_Parse(rc, packet); + + return rc; +} +#else +/* Submit the finalized command in packet (length cmdSz) and parse the + * response, returning the TPM response code. On TPM_RC_RETRY the TPM is + * momentarily busy (e.g. persisting the daUsed flag on first auth use of a + * non-noDA key) and asks for the identical command to be resubmitted; the + * error response is header-only, so restoring the command header and resending + * up to ctx->retries times recovers transparently. */ +static TPM_RC TPM2_TransmitCommand(TPM2_CTX* ctx, TPM2_Packet* packet, + UINT32 cmdSz) +{ + TPM_RC rc; + byte cmdHdr[TPM2_HEADER_SIZE]; + int origSize = packet->size; + int retries = ctx->retries; + + XMEMCPY(cmdHdr, packet->buf, TPM2_HEADER_SIZE); + + for (;;) { + /* send command requires packet->pos to be the total command length */ + packet->pos = cmdSz; + + rc = TPM2_DispatchCommand(ctx, packet); + if (rc != 0) + return rc; /* transport or SPDM error */ + + /* parse response header and extract the TPM response code */ + rc = TPM2_Packet_Parse(rc, packet); + + if (TPM2_Packet_RetryRestore(rc, &retries, packet, cmdHdr, origSize)) { + #ifdef DEBUG_WOLFTPM + printf("TPM_RC_RETRY: resubmitting command, %d retries left\n", + retries); + #endif + continue; + } + break; + } + + return rc; +} +#endif /* WOLFTPM_NO_RETRY */ + static TPM_RC TPM2_SendCommandAuth(TPM2_CTX* ctx, TPM2_Packet* packet, CmdInfo_t* info) { @@ -507,26 +594,8 @@ static TPM_RC TPM2_SendCommandAuth(TPM2_CTX* ctx, TPM2_Packet* packet, return rc; } - /* reset packet->pos to total command length (send command requires it) */ - packet->pos = cmdSz; - - /* submit command and wait for response */ -#ifdef WOLFTPM_SPDM - rc = TPM2_SPDM_SendCommand(ctx, packet); - if (rc >= 0) { - if (rc != TPM_RC_SUCCESS) - return rc; /* SPDM active but failed, do not retry cleartext */ - } - else /* rc < 0: SPDM not active, use normal transport */ -#endif - { - rc = (TPM_RC)INTERNAL_SEND_COMMAND(ctx, packet); - } - if (rc != 0) - return rc; - - /* parse response */ - rc = TPM2_Packet_Parse(rc, packet); + /* submit command and parse the response */ + rc = TPM2_TransmitCommand(ctx, packet, cmdSz); respSz = packet->size; /* restart the unmarshalling position */ @@ -559,22 +628,10 @@ static TPM_RC TPM2_SendCommand(TPM2_CTX* ctx, TPM2_Packet* packet) if (ctx == NULL || packet == NULL) return BAD_FUNC_ARG; -#ifdef WOLFTPM_SPDM - rc = TPM2_SPDM_SendCommand(ctx, packet); - if (rc >= 0) { - if (rc != TPM_RC_SUCCESS) - return rc; /* SPDM active but failed, do not retry cleartext */ - return TPM2_Packet_Parse(rc, packet); - } - /* rc < 0 means SPDM not active, fall through to normal transport */ -#endif + /* submit command and parse the response */ + rc = TPM2_TransmitCommand(ctx, packet, (UINT32)packet->pos); - /* submit command and wait for response */ - rc = (TPM_RC)INTERNAL_SEND_COMMAND(ctx, packet); - if (rc != 0) - return rc; - - return TPM2_Packet_Parse(rc, packet); + return rc; } #ifndef WOLFTPM2_NO_WOLFCRYPT @@ -711,6 +768,35 @@ TPM_RC TPM2_SetHalIoCb(TPM2_CTX* ctx, TPM2HalIoCb ioCb, void* userCtx) return rc; } +#ifndef WOLFTPM_NO_RETRY +TPM_RC TPM2_SetCommandRetries(TPM2_CTX* ctx, int retries) +{ + TPM_RC rc; + + if (ctx == NULL || retries < 0) { + return BAD_FUNC_ARG; + } + + rc = TPM2_AcquireLock(ctx); + if (rc == TPM_RC_SUCCESS) { + ctx->retries = retries; + + TPM2_ReleaseLock(ctx); + } + + return rc; +} + +int TPM2_GetCommandRetries(TPM2_CTX* ctx) +{ + if (ctx == NULL) { + return BAD_FUNC_ARG; + } + /* atomic int read, no lock needed; the setter takes the lock */ + return ctx->retries; +} +#endif /* !WOLFTPM_NO_RETRY */ + /* If timeoutTries <= 0 then it will not try and startup chip and will * use existing default locality */ TPM_RC TPM2_Init_ex(TPM2_CTX* ctx, TPM2HalIoCb ioCb, void* userCtx, @@ -724,6 +810,10 @@ TPM_RC TPM2_Init_ex(TPM2_CTX* ctx, TPM2HalIoCb ioCb, void* userCtx, XMEMSET(ctx, 0, sizeof(TPM2_CTX)); +#ifndef WOLFTPM_NO_RETRY + ctx->retries = WOLFTPM_MAX_RETRIES; +#endif + #ifndef WOLFTPM2_NO_WOLFCRYPT rc = TPM2_WolfCrypt_Init(); if (rc != 0) diff --git a/src/tpm2_packet.c b/src/tpm2_packet.c index 1c0125d1..d44bf5a7 100644 --- a/src/tpm2_packet.c +++ b/src/tpm2_packet.c @@ -1625,6 +1625,23 @@ TPM_RC TPM2_Packet_Parse(TPM_RC rc, TPM2_Packet* packet) return rc; } +#ifndef WOLFTPM_NO_RETRY +int TPM2_Packet_RetryRestore(TPM_RC rc, int* retries, TPM2_Packet* packet, + const byte* cmdHdr, int origSize) +{ + if (rc != TPM_RC_RETRY || retries == NULL || *retries <= 0 || + packet == NULL || packet->buf == NULL || cmdHdr == NULL) { + return 0; + } + (*retries)--; + /* A TPM_RC_RETRY response is header-only, so the command body survives; + * restore the clobbered header and the buffer size for an identical resend */ + XMEMCPY(packet->buf, cmdHdr, TPM2_HEADER_SIZE); + packet->size = origSize; + return 1; +} +#endif /* !WOLFTPM_NO_RETRY */ + int TPM2_Packet_Finalize(TPM2_Packet* packet, TPM_ST tag, TPM_CC cc) { word32 cmdSz = packet->pos; /* get total packet size */ diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 22e1d9bd..056c081e 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -2515,6 +2515,94 @@ static void test_wolfTPM2_VerifyHashTicket_DigestSize(void) #endif } +#ifndef WOLFTPM_NO_RETRY +/* Transparent TPM_RC_RETRY resubmit is configurable. Verify the default seeded + * by init, the setter/getter round-trip, and rejection of bad arguments. */ +static void test_TPM2_CommandRetries(void) +{ + int rc; + WOLFTPM2_DEV dev; + + XMEMSET(&dev, 0, sizeof(dev)); + + rc = TPM2_SetCommandRetries(NULL, 1); + AssertIntEQ(rc, BAD_FUNC_ARG); + rc = TPM2_GetCommandRetries(NULL); + AssertIntEQ(rc, BAD_FUNC_ARG); + rc = TPM2_SetCommandRetries(&dev.ctx, -1); + AssertIntEQ(rc, BAD_FUNC_ARG); + + /* retries is seeded before any HAL IO setup, so the default holds even on + * builds without a default IO callback where init returns an error */ + (void)TPM2_Init_minimal(&dev.ctx); + AssertIntEQ(TPM2_GetCommandRetries(&dev.ctx), WOLFTPM_MAX_RETRIES); + + rc = TPM2_SetCommandRetries(&dev.ctx, 0); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(TPM2_GetCommandRetries(&dev.ctx), 0); + + rc = TPM2_SetCommandRetries(&dev.ctx, 7); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(TPM2_GetCommandRetries(&dev.ctx), 7); + + TPM2_Cleanup(&dev.ctx); + + printf("Test TPM Wrapper:\tCommandRetries config:\t\tPassed\n"); +} + +/* Exercise the TPM_RC_RETRY resubmit bookkeeping directly: header/size restore, + * command-body preservation, retry-count decrement, and surfacing TPM_RC_RETRY + * once the count is exhausted - the parts of the loop most likely to regress. */ +static void test_TPM2_Packet_RetryRestore(void) +{ + TPM2_Packet packet; + byte buf[TPM2_HEADER_SIZE + 4]; + byte hdr[TPM2_HEADER_SIZE]; + int retries; + + XMEMSET(hdr, 0xAA, sizeof(hdr)); + XMEMSET(buf, 0x00, sizeof(buf)); + buf[TPM2_HEADER_SIZE] = 0xBB; /* command body byte, must survive a resend */ + packet.buf = buf; + packet.pos = 0; + packet.size = TPM2_HEADER_SIZE; /* shrunk by a prior TPM2_Packet_Parse */ + + /* A non-retry response must not resubmit and must leave state untouched */ + retries = 2; + AssertIntEQ(TPM2_Packet_RetryRestore(TPM_RC_SUCCESS, &retries, &packet, + hdr, (int)sizeof(buf)), 0); + AssertIntEQ(retries, 2); + AssertIntEQ(packet.size, TPM2_HEADER_SIZE); + + /* RETRY with budget: resubmit, header + size restored, body preserved */ + AssertIntEQ(TPM2_Packet_RetryRestore(TPM_RC_RETRY, &retries, &packet, + hdr, (int)sizeof(buf)), 1); + AssertIntEQ(retries, 1); + AssertIntEQ(packet.size, (int)sizeof(buf)); + AssertIntEQ(buf[0], 0xAA); + AssertIntEQ(buf[TPM2_HEADER_SIZE], 0xBB); + + /* Second RETRY exhausts the budget */ + AssertIntEQ(TPM2_Packet_RetryRestore(TPM_RC_RETRY, &retries, &packet, + hdr, (int)sizeof(buf)), 1); + AssertIntEQ(retries, 0); + + /* Budget exhausted: RETRY is surfaced to the caller, no resubmit */ + AssertIntEQ(TPM2_Packet_RetryRestore(TPM_RC_RETRY, &retries, &packet, + hdr, (int)sizeof(buf)), 0); + AssertIntEQ(retries, 0); + + /* NULL guards */ + retries = 1; + AssertIntEQ(TPM2_Packet_RetryRestore(TPM_RC_RETRY, NULL, &packet, + hdr, (int)sizeof(buf)), 0); + AssertIntEQ(TPM2_Packet_RetryRestore(TPM_RC_RETRY, &retries, NULL, + hdr, (int)sizeof(buf)), 0); + + printf("Test TPM Wrapper:\tRetryRestore logic:\t\tPassed\n"); +} +#endif /* !WOLFTPM_NO_RETRY */ + /* wolfTPM2_NVCreateAuthPolicy must derive nameAlg from authPolicySz so * the policy digest hash matches the index's nameAlg. Bug-mode hardcoded * SHA-256 nameAlg, which made SHA-384/SHA-512 policies unsatisfiable. @@ -5699,6 +5787,10 @@ int unit_tests(int argc, char *argv[]) test_wolfTPM2_GetKeyTemplate_ex_nameAlg(); test_wolfTPM2_SignHashScheme_DigestSize(); test_wolfTPM2_VerifyHashTicket_DigestSize(); +#ifndef WOLFTPM_NO_RETRY + test_TPM2_CommandRetries(); + test_TPM2_Packet_RetryRestore(); +#endif test_wolfTPM2_NVCreateAuthPolicy_NameAlg(); test_wolfTPM2_GetKeyTemplate_KeyedHash_Scheme(); test_wolfTPM2_LoadEccPublicKey_Ex(); diff --git a/wolftpm/tpm2.h b/wolftpm/tpm2.h index 12f81067..8d381423 100644 --- a/wolftpm/tpm2.h +++ b/wolftpm/tpm2.h @@ -2229,6 +2229,11 @@ typedef struct TPM2_CTX { void* spdmCtx; /* Pointer to WOLFTPM2_SPDM_CTX when session active */ unsigned int spdmOnlyDetected:1; /* TPM_RC_DISABLED from Startup */ #endif + +#ifndef WOLFTPM_NO_RETRY + /* Additional resubmit attempts on TPM_RC_RETRY (0 disables) */ + int retries; +#endif } TPM2_CTX; @@ -3773,6 +3778,36 @@ WOLFTPM_API TPM_RC TPM2_ChipStartup(TPM2_CTX* ctx, int timeoutTries); */ WOLFTPM_API TPM_RC TPM2_SetHalIoCb(TPM2_CTX* ctx, TPM2HalIoCb ioCb, void* userCtx); +#ifndef WOLFTPM_NO_RETRY +/*! + \ingroup TPM2_Proprietary + \brief Sets the number of times a command is transparently resubmitted on TPM_RC_RETRY + \brief The TPM returns TPM_RC_RETRY when momentarily busy (for example persisting the daUsed flag on first auth use of a non-noDA key). Disabled by default (WOLFTPM_MAX_RETRIES is 0); pass a count > 0 to enable transparent resubmit. A count of 0 returns TPM_RC_RETRY to the caller. + + \return TPM_RC_SUCCESS: successful + \return BAD_FUNC_ARG: the TPM2 context is NULL or retries is negative + + \param ctx pointer to a TPM2_CTX struct + \param retries number of additional resubmit attempts on TPM_RC_RETRY (0 disables) + + \sa TPM2_GetCommandRetries +*/ +WOLFTPM_API TPM_RC TPM2_SetCommandRetries(TPM2_CTX* ctx, int retries); + +/*! + \ingroup TPM2_Proprietary + \brief Returns the number of times a command is transparently resubmitted on TPM_RC_RETRY + + \return the configured retry count on success + \return BAD_FUNC_ARG: the TPM2 context is a NULL pointer + + \param ctx pointer to a TPM2_CTX struct + + \sa TPM2_SetCommandRetries +*/ +WOLFTPM_API int TPM2_GetCommandRetries(TPM2_CTX* ctx); +#endif /* !WOLFTPM_NO_RETRY */ + /*! \ingroup TPM2_Proprietary \brief Sets the structure holding the TPM Authorizations. diff --git a/wolftpm/tpm2_packet.h b/wolftpm/tpm2_packet.h index 88f6a52f..4c14c316 100644 --- a/wolftpm/tpm2_packet.h +++ b/wolftpm/tpm2_packet.h @@ -241,6 +241,10 @@ WOLFTPM_LOCAL void TPM2_Packet_ParseAttest(TPM2_Packet* packet, TPMS_ATTEST* out WOLFTPM_LOCAL TPM_RC TPM2_Packet_Parse(TPM_RC rc, TPM2_Packet* packet); WOLFTPM_LOCAL int TPM2_Packet_Finalize(TPM2_Packet* packet, TPM_ST tag, TPM_CC cc); +#ifndef WOLFTPM_NO_RETRY +WOLFTPM_TEST_API int TPM2_Packet_RetryRestore(TPM_RC rc, int* retries, + TPM2_Packet* packet, const byte* cmdHdr, int origSize); +#endif WOLFTPM_LOCAL int TPM2_GetCmdAuthCount(TPM2_CTX* ctx, const CmdInfo_t* info); diff --git a/wolftpm/tpm2_types.h b/wolftpm/tpm2_types.h index f8ab3fa4..42e10502 100644 --- a/wolftpm/tpm2_types.h +++ b/wolftpm/tpm2_types.h @@ -571,6 +571,20 @@ typedef int64_t INT64; #define TPM_SPI_WAIT_RETRY 50 #endif +/* Default number of times to transparently resubmit a command on TPM_RC_RETRY. + * The TPM returns TPM_RC_RETRY when momentarily busy (e.g. persisting the + * daUsed flag on first auth use of a non-noDA key). Disabled by default (0) to + * keep the raw TPM response code; opt in at runtime with TPM2_SetCommandRetries + * or at build time with -DWOLFTPM_MAX_RETRIES=N. Define WOLFTPM_NO_RETRY to + * compile the handling out entirely. */ +#ifdef WOLFTPM_NO_RETRY + #undef WOLFTPM_MAX_RETRIES + #define WOLFTPM_MAX_RETRIES 0 +#endif +#ifndef WOLFTPM_MAX_RETRIES +#define WOLFTPM_MAX_RETRIES 0 +#endif + #ifndef MAX_SYM_BLOCK_SIZE #define MAX_SYM_BLOCK_SIZE 20 #endif