Skip to content

Commit 1601a78

Browse files
authored
Merge pull request #541 from aidangarske/fwtpm-da-hardening
fwTPM: Dictionary Attack (DA) hardening and DA/noDA test coverage
2 parents 27b654d + a5dfa14 commit 1601a78

16 files changed

Lines changed: 1828 additions & 59 deletions

File tree

.github/workflows/fwtpm-test.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,39 @@ jobs:
300300
tests/*.log
301301
retention-days: 5
302302

303+
# ----------------------------------------------------------------
304+
# Dictionary Attack TPM_RC_RETRY (daUsed) end-to-end.
305+
# Builds the server with -DFWTPM_DA_USED_RETRY so it returns TPM_RC_RETRY on
306+
# the first DA-protected auth use, then runs only da_check -lockout (which
307+
# rides through RC_RETRY and exercises lockout/recovery). Kept separate from
308+
# the examples matrix because the RETRY emulation breaks non-retry-aware
309+
# clients sharing one server (e.g. unit.test).
310+
# ----------------------------------------------------------------
311+
fwtpm-da-retry:
312+
runs-on: ubuntu-latest
313+
steps:
314+
- name: Checkout wolfTPM
315+
uses: actions/checkout@v4
316+
317+
- name: Setup wolfSSL
318+
uses: ./.github/actions/setup-wolfssl
319+
with:
320+
configure-flags: --enable-wolftpm --enable-pkcallbacks --enable-keygen
321+
cflags: -DWC_RSA_NO_PADDING
322+
323+
- name: Build wolfTPM (FWTPM_DA_USED_RETRY)
324+
run: |
325+
./autogen.sh
326+
./configure --enable-fwtpm --enable-swtpm --enable-debug \
327+
CFLAGS="-DFWTPM_DA_USED_RETRY"
328+
make -j"$(nproc)"
329+
330+
- name: Run DA RC_RETRY end-to-end
331+
run: |
332+
./tests/fwtpm_da_retry.sh || rc=$?
333+
# 77 is the autotools "skip" code; treat it as a pass.
334+
[ "${rc:-0}" -eq 0 ] || [ "${rc:-0}" -eq 77 ]
335+
303336
# ----------------------------------------------------------------
304337
# tpm2-tools compatibility test against IBM SW TPM
305338
# Validates that tpm2_tools_test.sh works on a reference TPM.

ChangeLog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
`--enable-mlkem[=all|enc|dec|no]`, and `--disable-hash-mldsa` configure flags
1616
(mirroring the wolfSSL syntax). `--enable-pqc` now selects the lean PQC subset
1717
and excludes the non-PQC v1.85 spec code; `--enable-v185` is unchanged (full).
18+
* Hardened fwTPM Dictionary Attack (DA) protection to the TCG spec: `noDA` is now
19+
honored on objects (`TPMA_OBJECT_noDA`), not just NV indices; `failedTries` is
20+
persisted in NV with the non-orderly-shutdown +1 penalty; `recoveryTime`
21+
self-heal and a separate `lockoutRecovery` for lockoutAuth are implemented; and
22+
`TPM2_GetCapability` reports the DA properties (`TPM_PT_MAX_AUTH_FAIL`,
23+
`TPM_PT_LOCKOUT_INTERVAL`/`_RECOVERY`/`_COUNTER`, `TPM_PT_PERMANENT.inLockout`).
24+
New `FWTPM_DA_USED_RETRY` build macro emulates a real TPM returning
25+
`TPM_RC_RETRY` while it persists `daUsed` on the first DA-protected auth use.
26+
Added `wolfTPM2_DictionaryAttackLockReset` / `wolfTPM2_DictionaryAttackParameters`
27+
client wrappers, DA/noDA/lockout/self-heal/persistence unit tests, an
28+
`examples/management/da_check` end-to-end example, and the
29+
`tests/fwtpm_da_retry.sh` CI harness.
1830

1931
## wolfTPM Release 4.0.0 (Apr 22, 2026)
2032

docs/FWTPM.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,56 @@ existing state.
311311
| `TPM2_PolicyLocality` | Restrict policy to specific locality |
312312
| `TPM2_PolicySigned` | Authorize policy with external signing key |
313313

314+
### Dictionary Attack (DA) Protection
315+
316+
| Command | Description |
317+
|---------|-------------|
318+
| `TPM2_DictionaryAttackParameters` | Set `maxTries`, `recoveryTime`, `lockoutRecovery` |
319+
| `TPM2_DictionaryAttackLockReset` | Reset the failed-tries counter (lockoutAuth) |
320+
321+
fwTPM follows the TPM 2.0 spec (Part 1 Sec.19.8). A failed authorization of a
322+
DA-protected entity increments `failedTries`; once it reaches `maxTries` the TPM
323+
returns `TPM_RC_LOCKOUT`. `failedTries` is persisted in NV on every failure, so
324+
a power cycle cannot reset it. When a clock HAL is registered
325+
(`FWTPM_Clock_SetHAL`) it self-heals one try per `recoveryTime` seconds, and a
326+
non-orderly shutdown adds a one-try penalty; on clockless builds neither applies
327+
(recovery is via `DictionaryAttackLockReset`/`Clear` only) so routine unclean
328+
power-off cannot accumulate into lockout. A failed `lockoutAuth` locks the
329+
lockout hierarchy: that lock persists across reboot and clears after
330+
`lockoutRecovery` seconds, except when `lockoutRecovery` is 0 (reboot-only
331+
recovery). Because the clock HAL reports milliseconds *since boot*, this timer
332+
measures continuous post-boot uptime, not wall-clock time across reboots — a
333+
device that reboots more often than `lockoutRecovery` extends its effective
334+
recovery window. The lock only blocks commands authorized via `lockoutAuth`
335+
(`DictionaryAttackLockReset`, `DictionaryAttackParameters`, lockout-authorized
336+
`Clear`); the platform hierarchy is always an escape hatch —
337+
`TPM2_ClearControl(platformAuth, clearDisable=NO)` then `TPM2_Clear(platformAuth)`
338+
recovers even when `disableClear` was set. `Startup`/`Shutdown` are never
339+
DA-gated, so a reboot in lockout can always recover. Entities marked `noDA`
340+
(`TPMA_OBJECT_noDA` on objects,
341+
`TPMA_NV_NO_DA` on NV indices) never feed the counter and stay usable during
342+
lockout. `TPM2_GetCapability(TPM_CAP_TPM_PROPERTIES)` reports
343+
`TPM_PT_MAX_AUTH_FAIL`, `TPM_PT_LOCKOUT_INTERVAL`, `TPM_PT_LOCKOUT_RECOVERY`,
344+
`TPM_PT_LOCKOUT_COUNTER`, and the `inLockout` bit of `TPM_PT_PERMANENT`.
345+
346+
Durable accounting writes the NV FLAGS entry on each DA-protected failure (and
347+
on the first DA-protected auth use per boot). This is bounded per boot — the
348+
lockout gate stops counting once locked — but on flash-backed targets it adds
349+
wear and makes failed-auth latency NV-bound; size the NV backend accordingly.
350+
351+
The first use of a DA-protected (non-`noDA`) authorization after startup makes a
352+
real TPM persist a `daUsed` flag to NV and return `TPM_RC_RETRY` ("resubmit the
353+
identical command") while it writes. Build with `FWTPM_DA_USED_RETRY` to emulate
354+
this so clients exercise their resubmit/retry handling. It is off by default;
355+
DA accounting and persistence are active regardless. Compile out all DA logic
356+
with `FWTPM_NO_DA`.
357+
358+
Coverage: DA/noDA/lockout/self-heal/persistence unit tests in
359+
`tests/fwtpm_unit_tests.c`, the `examples/management/da_check` end-to-end example
360+
(add `-lockout` for the destructive lockout/recovery path), and the
361+
`tests/fwtpm_da_retry.sh` harness that exercises the `TPM_RC_RETRY` path against
362+
a `FWTPM_DA_USED_RETRY` build.
363+
314364
### NV RAM
315365

316366
| Command | Description |
@@ -453,6 +503,10 @@ All macros are compile-time overridable (e.g., `-DFWTPM_MAX_OBJECTS=8`).
453503
| `FWTPM_MAX_SESSIONS` | 8 | Maximum concurrent auth sessions |
454504
| `FWTPM_MAX_NV_INDICES` | 16 | Maximum NV RAM index slots |
455505
| `FWTPM_MAX_NV_DATA` | 2048 | Maximum data per NV index (bytes) |
506+
| `FWTPM_DA_DEFAULT_MAX_TRIES` | 32 | DA failed-auth count before lockout |
507+
| `FWTPM_DA_DEFAULT_RECOVERY` | 600 | DA self-heal interval (seconds per try) |
508+
| `FWTPM_DA_DEFAULT_LOCKOUT_RECOVERY` | 86400 | lockoutAuth recovery time (seconds) |
509+
| `FWTPM_DA_MAX_TRIES_LIMIT` | 0xFFFF | Upper clamp for a replayed `maxTries`/`failedTries` |
456510
| `FWTPM_MAX_DATA_BUF` | 1024 | Internal buffer for HMAC, hash, general data |
457511
| `FWTPM_MAX_PUB_BUF` | 512 | Internal buffer for public area, signatures |
458512
| `FWTPM_MAX_DER_SIG_BUF` | 256 | Internal buffer for DER signatures, ECC points |
@@ -554,6 +608,12 @@ to reduce code size on constrained targets.
554608
| `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` |
555609
| `FWTPM_NO_POLICY` | not defined | `PolicyGetDigest`, `PolicyRestart`, `PolicyPCR`, `PolicyPassword`, `PolicyAuthValue`, `PolicyCommandCode`, `PolicyOR`, `PolicySecret`, `PolicyAuthorize`, `PolicyNV` |
556610
| `FWTPM_NO_CREDENTIAL` | not defined | `MakeCredential`, `ActivateCredential` |
611+
| `FWTPM_NO_DA` | not defined | `DictionaryAttackParameters`, `DictionaryAttackLockReset`, and all lockout accounting |
612+
613+
The `FWTPM_DA_USED_RETRY` macro (off by default) does not remove commands; it
614+
makes the server return `TPM_RC_RETRY` on the first DA-protected auth use after
615+
startup, emulating a real TPM persisting `daUsed`. See
616+
[Dictionary Attack (DA) Protection](#dictionary-attack-da-protection).
557617
558618
**Minimal build example** (measured boot only):
559619

examples/management/da_check.c

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/* da_check.c
2+
*
3+
* Copyright (C) 2006-2026 wolfSSL Inc.
4+
*
5+
* This file is part of wolfTPM.
6+
*
7+
* wolfTPM is free software; you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation; either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* wolfTPM is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program; if not, write to the Free Software
19+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
20+
*/
21+
22+
/* Exercises Dictionary Attack (DA) vs noDA behavior end to end: signing with a
23+
* DA-protected (non-noDA) key rides through the TPM_RC_RETRY the TPM returns
24+
* while it persists the daUsed flag on first auth use, and a noDA key never
25+
* trips lockout. With -lockout it also drives the lockout/recovery path. */
26+
27+
#ifdef HAVE_CONFIG_H
28+
#include <config.h>
29+
#endif
30+
31+
#include <wolftpm/tpm2_wrap.h>
32+
#include <examples/management/management.h>
33+
#include <hal/tpm_io.h>
34+
#include <examples/tpm_test.h>
35+
36+
#include <stdio.h>
37+
38+
#if !defined(WOLFTPM2_NO_WRAPPER) && defined(HAVE_ECC)
39+
40+
/* Sign a digest, resubmitting on TPM_RC_RETRY. A real TPM (and fwTPM built
41+
* with FWTPM_DA_USED_RETRY) returns RETRY once while it persists daUsed on the
42+
* first DA-protected auth use. The wolfTPM client does not auto-resubmit, so
43+
* callers must resend the identical command, as this loop does. */
44+
static int DaSignWithRetry(WOLFTPM2_DEV* dev, WOLFTPM2_KEY* key,
45+
const byte* digest, int digestSz, byte* sig, int sigCap)
46+
{
47+
int rc;
48+
int sigSz;
49+
int retries = 0;
50+
51+
do {
52+
sigSz = sigCap;
53+
rc = wolfTPM2_SignHash(dev, key, digest, digestSz, sig, &sigSz);
54+
if (rc == TPM_RC_RETRY) {
55+
printf(" TPM_RC_RETRY (daUsed persist) - resubmitting\n");
56+
}
57+
} while (rc == TPM_RC_RETRY && ++retries < 10);
58+
59+
return rc;
60+
}
61+
62+
63+
int TPM2_DA_Check_Example(void* userCtx, int argc, char *argv[])
64+
{
65+
int rc;
66+
int i;
67+
int sigSz;
68+
int locked = 0;
69+
int doLockout = 0;
70+
WOLFTPM2_DEV dev;
71+
WOLFTPM2_KEY srk;
72+
WOLFTPM2_KEY daKey; /* DA-protected: noDA clear */
73+
WOLFTPM2_KEY noDaKey; /* noDA set */
74+
TPMT_PUBLIC publicTemplate;
75+
byte digest[TPM_SHA256_DIGEST_SIZE];
76+
byte sig[256];
77+
const byte keyAuth[] = { 'd', 'a', '-', 'a', 'u', 't', 'h' };
78+
79+
for (i = 1; i < argc; i++) {
80+
if (XSTRCMP(argv[i], "-lockout") == 0) {
81+
doLockout = 1;
82+
}
83+
}
84+
85+
XMEMSET(&dev, 0, sizeof(dev));
86+
XMEMSET(&srk, 0, sizeof(srk));
87+
XMEMSET(&daKey, 0, sizeof(daKey));
88+
XMEMSET(&noDaKey, 0, sizeof(noDaKey));
89+
XMEMSET(digest, 0x11, sizeof(digest));
90+
91+
printf("TPM2 Dictionary Attack (DA / noDA) check\n");
92+
93+
rc = wolfTPM2_Init(&dev, TPM2_IoCb, userCtx);
94+
if (rc != TPM_RC_SUCCESS) {
95+
printf("wolfTPM2_Init failed 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc));
96+
return rc;
97+
}
98+
99+
rc = wolfTPM2_CreateSRK(&dev, &srk, TPM_ALG_ECC, NULL, 0);
100+
if (rc != 0) goto exit;
101+
102+
/* DA-protected ECC signing key (noDA deliberately omitted). */
103+
rc = wolfTPM2_GetKeyTemplate_ECC(&publicTemplate,
104+
TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent |
105+
TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth |
106+
TPMA_OBJECT_sign,
107+
TPM_ECC_NIST_P256, TPM_ALG_ECDSA);
108+
if (rc != 0) goto exit;
109+
publicTemplate.nameAlg = TPM_ALG_SHA256;
110+
rc = wolfTPM2_CreateAndLoadKey(&dev, &daKey, &srk.handle, &publicTemplate,
111+
keyAuth, (int)sizeof(keyAuth));
112+
if (rc != 0) goto exit;
113+
printf("Created DA-protected ECC signing key (noDA clear)\n");
114+
115+
/* First auth use of a DA-protected key may return TPM_RC_RETRY. */
116+
rc = DaSignWithRetry(&dev, &daKey, digest, (int)sizeof(digest),
117+
sig, (int)sizeof(sig));
118+
if (rc != 0) goto exit;
119+
printf("Signed with DA-protected key (rode through any RC_RETRY)\n");
120+
121+
if (doLockout) {
122+
wolfTPM2_SetAuthPassword(&dev, 0, NULL);
123+
rc = wolfTPM2_DictionaryAttackParameters(&dev, 3, 0, 0);
124+
if (rc != 0) goto exit;
125+
126+
/* Bad auth until lockout. */
127+
daKey.handle.auth.buffer[0] ^= 0xFF;
128+
for (i = 0; i < 8 && !locked; i++) {
129+
sigSz = (int)sizeof(sig);
130+
rc = wolfTPM2_SignHash(&dev, &daKey, digest, (int)sizeof(digest),
131+
sig, &sigSz);
132+
if (rc == TPM_RC_LOCKOUT) {
133+
locked = 1;
134+
}
135+
}
136+
daKey.handle.auth.buffer[0] ^= 0xFF;
137+
if (!locked) {
138+
printf("Expected lockout was not reached\n");
139+
rc = TPM_RC_FAILURE;
140+
goto exit;
141+
}
142+
printf("Entered lockout after repeated bad auth\n");
143+
144+
wolfTPM2_SetAuthPassword(&dev, 0, NULL);
145+
rc = wolfTPM2_DictionaryAttackLockReset(&dev);
146+
if (rc != 0) goto exit;
147+
rc = DaSignWithRetry(&dev, &daKey, digest, (int)sizeof(digest),
148+
sig, (int)sizeof(sig));
149+
if (rc != 0) goto exit;
150+
printf("Recovered via DictionaryAttackLockReset; signing works\n");
151+
}
152+
153+
/* noDA contrast key. */
154+
rc = wolfTPM2_GetKeyTemplate_ECC(&publicTemplate,
155+
TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent |
156+
TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth |
157+
TPMA_OBJECT_sign | TPMA_OBJECT_noDA,
158+
TPM_ECC_NIST_P256, TPM_ALG_ECDSA);
159+
if (rc != 0) goto exit;
160+
publicTemplate.nameAlg = TPM_ALG_SHA256;
161+
rc = wolfTPM2_CreateAndLoadKey(&dev, &noDaKey, &srk.handle, &publicTemplate,
162+
keyAuth, (int)sizeof(keyAuth));
163+
if (rc != 0) goto exit;
164+
165+
/* Repeated bad auth on a noDA key must never reach lockout. */
166+
noDaKey.handle.auth.buffer[0] ^= 0xFF;
167+
for (i = 0; i < 8; i++) {
168+
sigSz = (int)sizeof(sig);
169+
rc = wolfTPM2_SignHash(&dev, &noDaKey, digest, (int)sizeof(digest),
170+
sig, &sigSz);
171+
if (rc == TPM_RC_LOCKOUT) {
172+
printf("noDA key unexpectedly hit lockout\n");
173+
noDaKey.handle.auth.buffer[0] ^= 0xFF;
174+
rc = TPM_RC_FAILURE;
175+
goto exit;
176+
}
177+
}
178+
noDaKey.handle.auth.buffer[0] ^= 0xFF;
179+
180+
/* A correct-auth noDA sign never returns RC_RETRY. */
181+
sigSz = (int)sizeof(sig);
182+
rc = wolfTPM2_SignHash(&dev, &noDaKey, digest, (int)sizeof(digest),
183+
sig, &sigSz);
184+
if (rc == TPM_RC_RETRY) {
185+
printf("noDA key unexpectedly returned RC_RETRY\n");
186+
rc = TPM_RC_FAILURE;
187+
goto exit;
188+
}
189+
if (rc != 0) goto exit;
190+
printf("noDA key: no lockout, no RC_RETRY (DA bypassed as expected)\n");
191+
192+
printf("DA check example complete\n");
193+
194+
exit:
195+
if (rc != 0) {
196+
printf("Failure 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc));
197+
}
198+
199+
/* Best-effort: do not leave the TPM locked for subsequent examples. */
200+
wolfTPM2_SetAuthPassword(&dev, 0, NULL);
201+
(void)wolfTPM2_DictionaryAttackLockReset(&dev);
202+
203+
wolfTPM2_UnloadHandle(&dev, &noDaKey.handle);
204+
wolfTPM2_UnloadHandle(&dev, &daKey.handle);
205+
wolfTPM2_UnloadHandle(&dev, &srk.handle);
206+
wolfTPM2_Cleanup(&dev);
207+
return rc;
208+
}
209+
#endif /* !WOLFTPM2_NO_WRAPPER && HAVE_ECC */
210+
211+
#ifndef NO_MAIN_DRIVER
212+
int main(int argc, char *argv[])
213+
{
214+
int rc = NOT_COMPILED_IN;
215+
216+
#if !defined(WOLFTPM2_NO_WRAPPER) && defined(HAVE_ECC)
217+
rc = TPM2_DA_Check_Example(NULL, argc, argv);
218+
#else
219+
printf("DA check tool requires the wrapper and ECC\n");
220+
(void)argc;
221+
(void)argv;
222+
#endif
223+
224+
return rc;
225+
}
226+
#endif

examples/management/include.am

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
if BUILD_EXAMPLES
55
noinst_PROGRAMS += examples/management/flush \
6-
examples/management/tpmclear
6+
examples/management/tpmclear \
7+
examples/management/da_check
78

89
noinst_HEADERS += examples/management/management.h
910

@@ -14,11 +15,17 @@ examples_management_flush_DEPENDENCIES = src/libwolftpm.la
1415
examples_management_tpmclear_SOURCES = examples/management/tpmclear.c
1516
examples_management_tpmclear_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
1617
examples_management_tpmclear_DEPENDENCIES = src/libwolftpm.la
18+
19+
examples_management_da_check_SOURCES = examples/management/da_check.c
20+
examples_management_da_check_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
21+
examples_management_da_check_DEPENDENCIES = src/libwolftpm.la
1722
endif
1823

1924
example_managementdir = $(exampledir)/management
2025
dist_example_management_DATA = examples/management/flush.c \
21-
examples/management/tpmclear.c
26+
examples/management/tpmclear.c \
27+
examples/management/da_check.c
2228

2329
DISTCLEANFILES+= examples/management/.libs/flush \
24-
examples/management/.libs/tpmclear
30+
examples/management/.libs/tpmclear \
31+
examples/management/.libs/da_check

0 commit comments

Comments
 (0)