Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/fwtpm-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,39 @@ jobs:
tests/*.log
retention-days: 5

# ----------------------------------------------------------------
# Dictionary Attack TPM_RC_RETRY (daUsed) end-to-end.
# Builds the server with -DFWTPM_DA_USED_RETRY so it returns TPM_RC_RETRY on
# the first DA-protected auth use, then runs only da_check -lockout (which
# rides through RC_RETRY and exercises lockout/recovery). Kept separate from
# the examples matrix because the RETRY emulation breaks non-retry-aware
# clients sharing one server (e.g. unit.test).
# ----------------------------------------------------------------
fwtpm-da-retry:
runs-on: ubuntu-latest
steps:
- name: Checkout wolfTPM
uses: actions/checkout@v4

- name: Setup wolfSSL
uses: ./.github/actions/setup-wolfssl
with:
configure-flags: --enable-wolftpm --enable-pkcallbacks --enable-keygen
cflags: -DWC_RSA_NO_PADDING

- name: Build wolfTPM (FWTPM_DA_USED_RETRY)
run: |
./autogen.sh
./configure --enable-fwtpm --enable-swtpm --enable-debug \
CFLAGS="-DFWTPM_DA_USED_RETRY"
make -j"$(nproc)"

- name: Run DA RC_RETRY end-to-end
run: |
./tests/fwtpm_da_retry.sh || rc=$?
# 77 is the autotools "skip" code; treat it as a pass.
[ "${rc:-0}" -eq 0 ] || [ "${rc:-0}" -eq 77 ]

# ----------------------------------------------------------------
# tpm2-tools compatibility test against IBM SW TPM
# Validates that tpm2_tools_test.sh works on a reference TPM.
Expand Down
12 changes: 12 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
`--enable-mlkem[=all|enc|dec|no]`, and `--disable-hash-mldsa` configure flags
(mirroring the wolfSSL syntax). `--enable-pqc` now selects the lean PQC subset
and excludes the non-PQC v1.85 spec code; `--enable-v185` is unchanged (full).
* Hardened fwTPM Dictionary Attack (DA) protection to the TCG spec: `noDA` is now
honored on objects (`TPMA_OBJECT_noDA`), not just NV indices; `failedTries` is
persisted in NV with the non-orderly-shutdown +1 penalty; `recoveryTime`
self-heal and a separate `lockoutRecovery` for lockoutAuth are implemented; and
`TPM2_GetCapability` reports the DA properties (`TPM_PT_MAX_AUTH_FAIL`,
`TPM_PT_LOCKOUT_INTERVAL`/`_RECOVERY`/`_COUNTER`, `TPM_PT_PERMANENT.inLockout`).
New `FWTPM_DA_USED_RETRY` build macro emulates a real TPM returning
`TPM_RC_RETRY` while it persists `daUsed` on the first DA-protected auth use.
Added `wolfTPM2_DictionaryAttackLockReset` / `wolfTPM2_DictionaryAttackParameters`
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.

## wolfTPM Release 4.0.0 (Apr 22, 2026)

Expand Down
60 changes: 60 additions & 0 deletions docs/FWTPM.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,56 @@ existing state.
| `TPM2_PolicyLocality` | Restrict policy to specific locality |
| `TPM2_PolicySigned` | Authorize policy with external signing key |

### Dictionary Attack (DA) Protection

| Command | Description |
|---------|-------------|
| `TPM2_DictionaryAttackParameters` | Set `maxTries`, `recoveryTime`, `lockoutRecovery` |
| `TPM2_DictionaryAttackLockReset` | Reset the failed-tries counter (lockoutAuth) |

fwTPM follows the TPM 2.0 spec (Part 1 Sec.19.8). A failed authorization of a
DA-protected entity increments `failedTries`; once it reaches `maxTries` the TPM
returns `TPM_RC_LOCKOUT`. `failedTries` is persisted in NV on every failure, so
a power cycle cannot reset it. When a clock HAL is registered
(`FWTPM_Clock_SetHAL`) it self-heals one try per `recoveryTime` seconds, and a
non-orderly shutdown adds a one-try penalty; on clockless builds neither applies
(recovery is via `DictionaryAttackLockReset`/`Clear` only) so routine unclean
power-off cannot accumulate into lockout. A failed `lockoutAuth` locks the
lockout hierarchy: that lock persists across reboot and clears after
`lockoutRecovery` seconds, except when `lockoutRecovery` is 0 (reboot-only
recovery). Because the clock HAL reports milliseconds *since boot*, this timer
measures continuous post-boot uptime, not wall-clock time across reboots — a
device that reboots more often than `lockoutRecovery` extends its effective
recovery window. The lock only blocks commands authorized via `lockoutAuth`
(`DictionaryAttackLockReset`, `DictionaryAttackParameters`, lockout-authorized
`Clear`); the platform hierarchy is always an escape hatch —
`TPM2_ClearControl(platformAuth, clearDisable=NO)` then `TPM2_Clear(platformAuth)`
recovers even when `disableClear` was set. `Startup`/`Shutdown` are never
DA-gated, so a reboot in lockout can always recover. Entities marked `noDA`
(`TPMA_OBJECT_noDA` on objects,
`TPMA_NV_NO_DA` on NV indices) never feed the counter and stay usable during
lockout. `TPM2_GetCapability(TPM_CAP_TPM_PROPERTIES)` reports
`TPM_PT_MAX_AUTH_FAIL`, `TPM_PT_LOCKOUT_INTERVAL`, `TPM_PT_LOCKOUT_RECOVERY`,
`TPM_PT_LOCKOUT_COUNTER`, and the `inLockout` bit of `TPM_PT_PERMANENT`.

Durable accounting writes the NV FLAGS entry on each DA-protected failure (and
on the first DA-protected auth use per boot). This is bounded per boot — the
lockout gate stops counting once locked — but on flash-backed targets it adds
wear and makes failed-auth latency NV-bound; size the NV backend accordingly.

The first use of a DA-protected (non-`noDA`) authorization after startup makes a
real TPM persist a `daUsed` flag to NV and return `TPM_RC_RETRY` ("resubmit the
identical command") while it writes. Build with `FWTPM_DA_USED_RETRY` to emulate
this so clients exercise their resubmit/retry handling. It is off by default;
DA accounting and persistence are active regardless. Compile out all DA logic
with `FWTPM_NO_DA`.

Coverage: DA/noDA/lockout/self-heal/persistence unit tests in
`tests/fwtpm_unit_tests.c`, the `examples/management/da_check` end-to-end example
(add `-lockout` for the destructive lockout/recovery path), and the
`tests/fwtpm_da_retry.sh` harness that exercises the `TPM_RC_RETRY` path against
a `FWTPM_DA_USED_RETRY` build.

### NV RAM

| Command | Description |
Expand Down Expand Up @@ -453,6 +503,10 @@ All macros are compile-time overridable (e.g., `-DFWTPM_MAX_OBJECTS=8`).
| `FWTPM_MAX_SESSIONS` | 8 | Maximum concurrent auth sessions |
| `FWTPM_MAX_NV_INDICES` | 16 | Maximum NV RAM index slots |
| `FWTPM_MAX_NV_DATA` | 2048 | Maximum data per NV index (bytes) |
| `FWTPM_DA_DEFAULT_MAX_TRIES` | 32 | DA failed-auth count before lockout |
| `FWTPM_DA_DEFAULT_RECOVERY` | 600 | DA self-heal interval (seconds per try) |
| `FWTPM_DA_DEFAULT_LOCKOUT_RECOVERY` | 86400 | lockoutAuth recovery time (seconds) |
| `FWTPM_DA_MAX_TRIES_LIMIT` | 0xFFFF | Upper clamp for a replayed `maxTries`/`failedTries` |
| `FWTPM_MAX_DATA_BUF` | 1024 | Internal buffer for HMAC, hash, general data |
| `FWTPM_MAX_PUB_BUF` | 512 | Internal buffer for public area, signatures |
| `FWTPM_MAX_DER_SIG_BUF` | 256 | Internal buffer for DER signatures, ECC points |
Expand Down Expand Up @@ -554,6 +608,12 @@ to reduce code size on constrained targets.
| `FWTPM_NO_NV` | not defined | `NV_DefineSpace`, `NV_UndefineSpace`, `NV_ReadPublic`, `NV_Write`, `NV_Read`, `NV_Extend`, `NV_Increment`, `NV_WriteLock`, `NV_ReadLock`, `NV_Certify` |
| `FWTPM_NO_POLICY` | not defined | `PolicyGetDigest`, `PolicyRestart`, `PolicyPCR`, `PolicyPassword`, `PolicyAuthValue`, `PolicyCommandCode`, `PolicyOR`, `PolicySecret`, `PolicyAuthorize`, `PolicyNV` |
| `FWTPM_NO_CREDENTIAL` | not defined | `MakeCredential`, `ActivateCredential` |
| `FWTPM_NO_DA` | not defined | `DictionaryAttackParameters`, `DictionaryAttackLockReset`, and all lockout accounting |

The `FWTPM_DA_USED_RETRY` macro (off by default) does not remove commands; it
makes the server return `TPM_RC_RETRY` on the first DA-protected auth use after
startup, emulating a real TPM persisting `daUsed`. See
[Dictionary Attack (DA) Protection](#dictionary-attack-da-protection).

**Minimal build example** (measured boot only):

Expand Down
226 changes: 226 additions & 0 deletions examples/management/da_check.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/* da_check.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
*/

/* Exercises Dictionary Attack (DA) vs noDA behavior end to end: signing with a
* DA-protected (non-noDA) key rides through the TPM_RC_RETRY the TPM returns
* while it persists the daUsed flag on first auth use, and a noDA key never
* trips lockout. With -lockout it also drives the lockout/recovery path. */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <wolftpm/tpm2_wrap.h>
#include <examples/management/management.h>
#include <hal/tpm_io.h>
#include <examples/tpm_test.h>

#include <stdio.h>

#if !defined(WOLFTPM2_NO_WRAPPER) && defined(HAVE_ECC)

/* Sign a digest, resubmitting on TPM_RC_RETRY. A real TPM (and fwTPM built
* with FWTPM_DA_USED_RETRY) returns RETRY once while it persists daUsed on the
* first DA-protected auth use. The wolfTPM client does not auto-resubmit, so
* callers must resend the identical command, as this loop does. */
static int DaSignWithRetry(WOLFTPM2_DEV* dev, WOLFTPM2_KEY* key,
const byte* digest, int digestSz, byte* sig, int sigCap)
{
int rc;
int sigSz;
int retries = 0;

do {
sigSz = sigCap;
rc = wolfTPM2_SignHash(dev, key, digest, digestSz, sig, &sigSz);
if (rc == TPM_RC_RETRY) {
printf(" TPM_RC_RETRY (daUsed persist) - resubmitting\n");
}
} while (rc == TPM_RC_RETRY && ++retries < 10);

return rc;
}


int TPM2_DA_Check_Example(void* userCtx, int argc, char *argv[])
{
int rc;
int i;
int sigSz;
int locked = 0;
int doLockout = 0;
WOLFTPM2_DEV dev;
WOLFTPM2_KEY srk;
WOLFTPM2_KEY daKey; /* DA-protected: noDA clear */
WOLFTPM2_KEY noDaKey; /* noDA set */
TPMT_PUBLIC publicTemplate;
byte digest[TPM_SHA256_DIGEST_SIZE];
byte sig[256];
const byte keyAuth[] = { 'd', 'a', '-', 'a', 'u', 't', 'h' };

for (i = 1; i < argc; i++) {
if (XSTRCMP(argv[i], "-lockout") == 0) {
doLockout = 1;
}
}

XMEMSET(&dev, 0, sizeof(dev));
XMEMSET(&srk, 0, sizeof(srk));
XMEMSET(&daKey, 0, sizeof(daKey));
XMEMSET(&noDaKey, 0, sizeof(noDaKey));
XMEMSET(digest, 0x11, sizeof(digest));

printf("TPM2 Dictionary Attack (DA / noDA) check\n");

rc = wolfTPM2_Init(&dev, TPM2_IoCb, userCtx);
if (rc != TPM_RC_SUCCESS) {
printf("wolfTPM2_Init failed 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc));
return rc;
}

rc = wolfTPM2_CreateSRK(&dev, &srk, TPM_ALG_ECC, NULL, 0);
if (rc != 0) goto exit;

/* DA-protected ECC signing key (noDA deliberately omitted). */
rc = wolfTPM2_GetKeyTemplate_ECC(&publicTemplate,
TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent |
TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth |
TPMA_OBJECT_sign,
TPM_ECC_NIST_P256, TPM_ALG_ECDSA);
if (rc != 0) goto exit;
publicTemplate.nameAlg = TPM_ALG_SHA256;
rc = wolfTPM2_CreateAndLoadKey(&dev, &daKey, &srk.handle, &publicTemplate,
keyAuth, (int)sizeof(keyAuth));
if (rc != 0) goto exit;
printf("Created DA-protected ECC signing key (noDA clear)\n");

/* First auth use of a DA-protected key may return TPM_RC_RETRY. */
rc = DaSignWithRetry(&dev, &daKey, digest, (int)sizeof(digest),
sig, (int)sizeof(sig));
if (rc != 0) goto exit;
printf("Signed with DA-protected key (rode through any RC_RETRY)\n");

if (doLockout) {
wolfTPM2_SetAuthPassword(&dev, 0, NULL);
rc = wolfTPM2_DictionaryAttackParameters(&dev, 3, 0, 0);
if (rc != 0) goto exit;

/* Bad auth until lockout. */
daKey.handle.auth.buffer[0] ^= 0xFF;
for (i = 0; i < 8 && !locked; i++) {
sigSz = (int)sizeof(sig);
rc = wolfTPM2_SignHash(&dev, &daKey, digest, (int)sizeof(digest),
sig, &sigSz);
if (rc == TPM_RC_LOCKOUT) {
locked = 1;
}
}
daKey.handle.auth.buffer[0] ^= 0xFF;
if (!locked) {
printf("Expected lockout was not reached\n");
rc = TPM_RC_FAILURE;
goto exit;
}
printf("Entered lockout after repeated bad auth\n");

wolfTPM2_SetAuthPassword(&dev, 0, NULL);
rc = wolfTPM2_DictionaryAttackLockReset(&dev);
if (rc != 0) goto exit;
rc = DaSignWithRetry(&dev, &daKey, digest, (int)sizeof(digest),
sig, (int)sizeof(sig));
if (rc != 0) goto exit;
printf("Recovered via DictionaryAttackLockReset; signing works\n");
}

/* noDA contrast key. */
rc = wolfTPM2_GetKeyTemplate_ECC(&publicTemplate,
TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent |
TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth |
TPMA_OBJECT_sign | TPMA_OBJECT_noDA,
TPM_ECC_NIST_P256, TPM_ALG_ECDSA);
if (rc != 0) goto exit;
publicTemplate.nameAlg = TPM_ALG_SHA256;
rc = wolfTPM2_CreateAndLoadKey(&dev, &noDaKey, &srk.handle, &publicTemplate,
keyAuth, (int)sizeof(keyAuth));
if (rc != 0) goto exit;

/* Repeated bad auth on a noDA key must never reach lockout. */
noDaKey.handle.auth.buffer[0] ^= 0xFF;
for (i = 0; i < 8; i++) {
sigSz = (int)sizeof(sig);
rc = wolfTPM2_SignHash(&dev, &noDaKey, digest, (int)sizeof(digest),
sig, &sigSz);
if (rc == TPM_RC_LOCKOUT) {
printf("noDA key unexpectedly hit lockout\n");
noDaKey.handle.auth.buffer[0] ^= 0xFF;
rc = TPM_RC_FAILURE;
goto exit;
}
}
noDaKey.handle.auth.buffer[0] ^= 0xFF;

/* A correct-auth noDA sign never returns RC_RETRY. */
sigSz = (int)sizeof(sig);
rc = wolfTPM2_SignHash(&dev, &noDaKey, digest, (int)sizeof(digest),
sig, &sigSz);
if (rc == TPM_RC_RETRY) {
printf("noDA key unexpectedly returned RC_RETRY\n");
rc = TPM_RC_FAILURE;
goto exit;
}
if (rc != 0) goto exit;
printf("noDA key: no lockout, no RC_RETRY (DA bypassed as expected)\n");

printf("DA check example complete\n");

exit:
if (rc != 0) {
printf("Failure 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc));
}

/* Best-effort: do not leave the TPM locked for subsequent examples. */
wolfTPM2_SetAuthPassword(&dev, 0, NULL);
(void)wolfTPM2_DictionaryAttackLockReset(&dev);

wolfTPM2_UnloadHandle(&dev, &noDaKey.handle);
wolfTPM2_UnloadHandle(&dev, &daKey.handle);
wolfTPM2_UnloadHandle(&dev, &srk.handle);
wolfTPM2_Cleanup(&dev);
return rc;
}
#endif /* !WOLFTPM2_NO_WRAPPER && HAVE_ECC */

#ifndef NO_MAIN_DRIVER
int main(int argc, char *argv[])
{
int rc = NOT_COMPILED_IN;

#if !defined(WOLFTPM2_NO_WRAPPER) && defined(HAVE_ECC)
rc = TPM2_DA_Check_Example(NULL, argc, argv);
#else
printf("DA check tool requires the wrapper and ECC\n");
(void)argc;
(void)argv;
#endif

return rc;
}
#endif
13 changes: 10 additions & 3 deletions examples/management/include.am
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

if BUILD_EXAMPLES
noinst_PROGRAMS += examples/management/flush \
examples/management/tpmclear
examples/management/tpmclear \
examples/management/da_check

noinst_HEADERS += examples/management/management.h

Expand All @@ -14,11 +15,17 @@ examples_management_flush_DEPENDENCIES = src/libwolftpm.la
examples_management_tpmclear_SOURCES = examples/management/tpmclear.c
examples_management_tpmclear_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
examples_management_tpmclear_DEPENDENCIES = src/libwolftpm.la

examples_management_da_check_SOURCES = examples/management/da_check.c
examples_management_da_check_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
examples_management_da_check_DEPENDENCIES = src/libwolftpm.la
endif

example_managementdir = $(exampledir)/management
dist_example_management_DATA = examples/management/flush.c \
examples/management/tpmclear.c
examples/management/tpmclear.c \
examples/management/da_check.c

DISTCLEANFILES+= examples/management/.libs/flush \
examples/management/.libs/tpmclear
examples/management/.libs/tpmclear \
examples/management/.libs/da_check
Loading
Loading