Merge pull request #541 from aidangarske/fwtpm-da-hardening

fwTPM: Dictionary Attack (DA) hardening and DA/noDA test coverage
pull/542/head
David Garske 2026-06-25 15:11:51 -07:00 committed by GitHub
commit 1601a78890
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1828 additions and 59 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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 |
@ -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 |
@ -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):

View File

@ -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

View File

@ -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
@ -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

View File

@ -28,6 +28,7 @@
int TPM2_Flush_Tool(void* userCtx, int argc, char *argv[]);
int TPM2_Clear_Tool(void* userCtx, int argc, char *argv[]);
int TPM2_DA_Check_Example(void* userCtx, int argc, char *argv[]);
#ifdef __cplusplus
} /* extern "C" */

View File

@ -115,6 +115,18 @@ RESULT=$?
RESULT=$?
[ $RESULT -ne 0 ] && echo -e "create primary owner rsa key stored failed! $RESULT" && exit 1
# Dictionary Attack (DA / noDA) check. Signs with a DA-protected key (rides
# through any TPM_RC_RETRY) and shows a noDA key never trips lockout. The
# destructive lockout path is opt-in via -lockout and not run here. Requires the
# wrapper and ECC (its body is compiled out otherwise).
if [ $WOLFCRYPT_ENABLE -eq 1 ] && [ $WOLFCRYPT_ECC -eq 1 ] && \
[ -x ./examples/management/da_check ]; then
echo -e "Dictionary Attack (DA / noDA) check"
./examples/management/da_check >> $TPMPWD/run.out 2>&1
RESULT=$?
[ $RESULT -ne 0 ] && echo -e "da_check failed! $RESULT" && exit 1
fi
if [ $WOLFCRYPT_ENABLE -eq 1 ]; then
# Provisioning examples (required --enable-provisioning)
./examples/keygen/create_primary -rsa -eh -iak -keep >> $TPMPWD/run.out 2>&1

View File

@ -73,6 +73,9 @@ static TPM_RC FwNvCheckAccess(TPM_HANDLE authHandle,
TPMI_RH_NV_INDEX nvHandle, UINT32 attributes, int isWrite);
#endif
static FWTPM_Object* FwFindObject(FWTPM_CTX* ctx, TPM_HANDLE handle);
#ifndef FWTPM_NO_DA
static UINT64 FwDaNowMs(FWTPM_CTX* ctx);
#endif
#ifdef WOLFTPM_MLDSA
static FWTPM_SignSeq* FwFindSignSeq(FWTPM_CTX* ctx, TPM_HANDLE handle);
#endif
@ -676,6 +679,33 @@ static TPM_RC FwCmd_Startup(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize,
}
if (rc == 0) {
#ifndef FWTPM_NO_DA
/* Non-orderly shutdown penalty (Part 1 Sec.19.8.5): if the prior
* session armed DA tracking but did not shut down cleanly, charge one
* failed try so a power-cycle cannot reset the lockout for free. Only
* applied when a clock HAL is present; clockless targets cannot
* self-heal, so charging the penalty there would let routine unclean
* power-off accumulate into lockout. */
if (ctx->clockHal.get_ms != NULL &&
!ctx->orderly && ctx->daFailedTries < ctx->daMaxTries) {
ctx->daFailedTries++;
}
ctx->orderly = 1;
ctx->daUsed = 0;
/* The failed-lockoutAuth lock recovers on reboot when lockoutRecovery
* is 0, or when there is no clock HAL to time-heal it (clockless
* targets must keep a reboot recovery path). Otherwise it persists and
* recovers on the timer, so a power-cycle cannot brute-force
* lockoutAuth. The clock HAL is ms-since-boot, so re-seeding the heal
* baseline below measures recovery as continuous post-boot uptime; a
* power-cycle restarts the window rather than clearing the lock. */
if (ctx->daLockoutRecovery == 0 || ctx->clockHal.get_ms == NULL) {
ctx->lockoutAuthFailed = 0;
}
ctx->daSelfHealMs = FwDaNowMs(ctx);
ctx->daLockoutHealMs = FwDaNowMs(ctx);
#endif
if (startupType == TPM_SU_CLEAR) {
/* Flush all transient objects, sessions, hash sequences,
* primary cache, and reset PCRs */
@ -725,16 +755,19 @@ static TPM_RC FwCmd_Startup(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize,
else {
/* TPM Restart/Resume: bump volatile restartCount */
ctx->restartCount++;
rc = FWTPM_NV_SaveFlags(ctx);
}
ctx->wasStarted = 1;
#ifdef DEBUG_WOLFTPM
printf("fwTPM: Startup(%s)\n",
startupType == TPM_SU_CLEAR ? "CLEAR" : "STATE");
#endif
FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS);
/* Only report success and mark the TPM started if the state writes
* above succeeded; otherwise the non-zero rc yields an error response. */
if (rc == 0) {
ctx->wasStarted = 1;
#ifdef DEBUG_WOLFTPM
printf("fwTPM: Startup(%s)\n",
startupType == TPM_SU_CLEAR ? "CLEAR" : "STATE");
#endif
FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS);
}
}
return rc;
@ -774,6 +807,11 @@ static TPM_RC FwCmd_Shutdown(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize,
FwFlushAllSessions(ctx);
}
#ifndef FWTPM_NO_DA
/* Mark a clean orderly checkpoint so the next startup applies no
* non-orderly DA penalty. */
ctx->orderly = 1;
#endif
rc = FWTPM_NV_Save(ctx);
if (rc != TPM_RC_SUCCESS) {
return rc;
@ -1213,11 +1251,18 @@ static TPM_RC FwCmd_GetCapability(FWTPM_CTX* ctx, TPM2_Packet* cmd,
#endif
0 },
#endif
{ TPM_PT_PERMANENT, 0 }, /* patched at emission */
{ TPM_PT_HR_LOADED, 0 },
{ TPM_PT_HR_LOADED_AVAIL, FWTPM_MAX_OBJECTS },
{ TPM_PT_HR_TRANSIENT_AVAIL, 0 },
{ TPM_PT_HR_PERSISTENT, 0 },
{ TPM_PT_HR_PERSISTENT_AVAIL, FWTPM_MAX_PERSISTENT },
#ifndef FWTPM_NO_DA
{ TPM_PT_LOCKOUT_COUNTER, 0 }, /* patched: daFailedTries */
{ TPM_PT_MAX_AUTH_FAIL, 0 }, /* patched: daMaxTries */
{ TPM_PT_LOCKOUT_INTERVAL, 0 }, /* patched: daRecoveryTime */
{ TPM_PT_LOCKOUT_RECOVERY, 0 }, /* patched: daLockoutRecovery */
#endif
};
int totalProps = (int)(sizeof(allProps) / sizeof(allProps[0]));
int startIdx = 0;
@ -1239,6 +1284,32 @@ static TPM_RC FwCmd_GetCapability(FWTPM_CTX* ctx, TPM2_Packet* cmd,
UINT32 val = allProps[startIdx + i].val;
if (prop == TPM_PT_TOTAL_COMMANDS)
val = (UINT32)FwGetCmdCount();
else if (prop == TPM_PT_PERMANENT) {
val = 0;
if (ctx->ownerAuth.size > 0)
val |= TPMA_PERMANENT_ownerAuthSet;
if (ctx->endorsementAuth.size > 0)
val |= TPMA_PERMANENT_endorsementAuthSet;
if (ctx->lockoutAuth.size > 0)
val |= TPMA_PERMANENT_lockoutAuthSet;
if (ctx->disableClear)
val |= TPMA_PERMANENT_disableClear;
#ifndef FWTPM_NO_DA
if (ctx->daMaxTries > 0 &&
ctx->daFailedTries >= ctx->daMaxTries)
val |= TPMA_PERMANENT_inLockout;
#endif
}
#ifndef FWTPM_NO_DA
else if (prop == TPM_PT_LOCKOUT_COUNTER)
val = ctx->daFailedTries;
else if (prop == TPM_PT_MAX_AUTH_FAIL)
val = ctx->daMaxTries;
else if (prop == TPM_PT_LOCKOUT_INTERVAL)
val = ctx->daRecoveryTime;
else if (prop == TPM_PT_LOCKOUT_RECOVERY)
val = ctx->daLockoutRecovery;
#endif
TPM2_Packet_AppendU32(rsp, prop);
TPM2_Packet_AppendU32(rsp, val);
}
@ -3424,6 +3495,27 @@ static TPM_RC FwCmd_Clear(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize,
/* Reset disableClear per spec */
ctx->disableClear = 0;
#ifndef FWTPM_NO_DA
/* Clear resets lockoutAuth and DA state to defaults
* (Part 3 Sec.24.6) */
TPM2_ForceZero(ctx->lockoutAuth.buffer,
sizeof(ctx->lockoutAuth.buffer));
ctx->lockoutAuth.size = 0;
ctx->daFailedTries = 0;
ctx->daMaxTries = FWTPM_DA_DEFAULT_MAX_TRIES;
ctx->daRecoveryTime = FWTPM_DA_DEFAULT_RECOVERY;
ctx->daLockoutRecovery = FWTPM_DA_DEFAULT_LOCKOUT_RECOVERY;
ctx->daUsed = 0;
ctx->orderly = 1;
ctx->lockoutAuthFailed = 0;
ctx->daSelfHealMs = FwDaNowMs(ctx);
ctx->daLockoutHealMs = 0;
/* Persist the cleared lockoutAuth and the DA flags so a Clear
* survives an unclean reboot. */
(void)FWTPM_NV_SaveAuth(ctx, TPM_RH_LOCKOUT);
(void)FWTPM_NV_SaveFlags(ctx);
#endif
FwRspNoParams(rsp, cmdTag);
}
else {
@ -11721,6 +11813,10 @@ static TPM_RC FwCmd_DictionaryAttackLockReset(FWTPM_CTX* ctx,
printf("fwTPM: DictionaryAttackLockReset\n");
#endif
ctx->daFailedTries = 0;
ctx->lockoutAuthFailed = 0;
ctx->daLockoutHealMs = 0;
ctx->daSelfHealMs = FwDaNowMs(ctx);
(void)FWTPM_NV_SaveFlags(ctx);
FwRspNoParams(rsp, cmdTag);
}
@ -11767,7 +11863,12 @@ static TPM_RC FwCmd_DictionaryAttackParameters(FWTPM_CTX* ctx,
ctx->daMaxTries = newMaxTries;
ctx->daRecoveryTime = newRecoveryTime;
ctx->daLockoutRecovery = lockoutRecovery;
FWTPM_NV_SaveFlags(ctx);
/* Per TCG Part 3, setting DA parameters also resets the counter. */
ctx->daFailedTries = 0;
ctx->lockoutAuthFailed = 0;
ctx->daLockoutHealMs = 0;
ctx->daSelfHealMs = FwDaNowMs(ctx);
(void)FWTPM_NV_SaveFlags(ctx);
FwRspNoParams(rsp, cmdTag);
}
@ -15693,10 +15794,21 @@ static const FWTPM_CMD_ENTRY* FwFindCmdEntry(TPM_CC cc)
}
#ifndef FWTPM_NO_DA
/* Return 1 if the handle is an NV index with TPMA_NV_NO_DA set. Such an
* index is exempt from dictionary-attack lockout (Part 1 Sec.23.2). */
/* Return 1 if the handle names an entity exempt from dictionary-attack
* lockout: an NV index with TPMA_NV_NO_DA, or an object with
* TPMA_OBJECT_noDA (Part 1 Sec.19.8.4). Such entities never feed the
* failed-tries counter and remain usable during lockout. */
static int FwHandleIsNoDA(FWTPM_CTX* ctx, TPM_HANDLE handle)
{
FWTPM_Object* obj;
if ((handle & 0xFF000000) == (TRANSIENT_FIRST & 0xFF000000) ||
(handle & 0xFF000000) == (PERSISTENT_FIRST & 0xFF000000)) {
obj = FwFindObject(ctx, handle);
if (obj != NULL && (obj->pub.objectAttributes & TPMA_OBJECT_noDA)) {
return 1;
}
}
#ifndef FWTPM_NO_NV
if ((handle & 0xFF000000) == (NV_INDEX_FIRST & 0xFF000000)) {
FWTPM_NvIndex* nv = FwFindNvIndex(ctx, handle);
@ -15704,11 +15816,108 @@ static int FwHandleIsNoDA(FWTPM_CTX* ctx, TPM_HANDLE handle)
return 1;
}
}
#else
(void)ctx; (void)handle;
#endif
return 0;
}
/* DA timing uses the volatile, reset-on-boot millisecond source (the raw clock
* HAL), NOT FWTPM_Clock_GetMs: the latter adds the persisted clockOffset, which
* TPM2_ClockSet can advance arbitrarily and which survives reboot either
* would let time-based self-heal wipe the persisted lockout (Part 1 Sec.19.8.3
* drives DA from Time, not Clock). Returns 0 when no clock HAL is registered. */
static UINT64 FwDaNowMs(FWTPM_CTX* ctx)
{
if (ctx->clockHal.get_ms != NULL) {
return ctx->clockHal.get_ms(ctx->clockHal.ctx);
}
return 0;
}
/* Self-heal the DA state as time passes (Part 1 Sec.19.8.3). One failed try
* is forgiven every daRecoveryTime seconds; the lockoutAuth lock clears after
* daLockoutRecovery seconds. Requires a clock HAL (FWTPM_Clock_SetHAL); with
* no time source recovery is reboot-only. Reductions are intentionally not
* persisted: a non-orderly reload re-derives them conservatively from the
* last durable counter (fail-safe). */
static void FwDaSelfHeal(FWTPM_CTX* ctx)
{
UINT64 now;
UINT64 elapsedSec, heals;
/* Only after Startup, which seeds the baselines: running before then would
* measure elapsed time from an unseeded (zero) baseline and wrongly heal.
* No clock HAL means reboot/LockReset-only recovery. */
if (!ctx->wasStarted || ctx->clockHal.get_ms == NULL) {
return;
}
now = FwDaNowMs(ctx);
if (ctx->daFailedTries == 0 || ctx->daRecoveryTime == 0) {
ctx->daSelfHealMs = now;
}
else if (now > ctx->daSelfHealMs) {
elapsedSec = (now - ctx->daSelfHealMs) / 1000;
heals = elapsedSec / ctx->daRecoveryTime;
if (heals >= (UINT64)ctx->daFailedTries) {
ctx->daFailedTries = 0;
ctx->daSelfHealMs = now;
}
else if (heals > 0) {
ctx->daFailedTries -= (UINT32)heals;
ctx->daSelfHealMs += heals * (UINT64)ctx->daRecoveryTime * 1000;
}
}
if (ctx->lockoutAuthFailed && ctx->daLockoutRecovery != 0 &&
now > ctx->daLockoutHealMs &&
(now - ctx->daLockoutHealMs) / 1000 >=
(UINT64)ctx->daLockoutRecovery) {
ctx->lockoutAuthFailed = 0;
ctx->daLockoutHealMs = 0;
}
}
/* Record an authorization failure for DA accounting. Returns 1 only when the
* failing command must itself be answered with TPM_RC_LOCKOUT (the failedTries
* threshold was just reached); otherwise returns 0 (the caller reports
* TPM_RC_AUTH_FAIL). The platform hierarchy is exempt (Part 1 Sec.19.8); a
* failed lockoutAuth arms the lockout-hierarchy lock once but still reports
* AUTH_FAIL subsequent reset/parameter attempts are rejected by the gate. */
static int FwDaRegisterFailure(FWTPM_CTX* ctx, TPM_HANDLE entityH)
{
/* The platform hierarchy is not DA-protected: a failed platformAuth must
* not feed the counter or trigger lockout. */
if (entityH == TPM_RH_PLATFORM) {
return 0;
}
if (entityH == TPM_RH_LOCKOUT) {
/* Record the lock once per episode. Repeated failures must not reset
* the recovery timer or append a journal entry each time, which would
* be a flash-wear / lock-extension DoS via wrong-auth Clear/LockReset. */
if (!ctx->lockoutAuthFailed) {
ctx->lockoutAuthFailed = 1;
ctx->daLockoutHealMs = FwDaNowMs(ctx);
ctx->orderly = 0;
(void)FWTPM_NV_SaveFlags(ctx);
}
return 0;
}
if (!FwHandleIsNoDA(ctx, entityH)) {
/* Cap at maxTries so a command that bypasses the lockout gate (e.g. a
* noDA primary handle with a DA-protected secondary auth) cannot grow
* the counter or append NV entries without bound while already
* locked. */
if (ctx->daFailedTries < ctx->daMaxTries) {
ctx->daFailedTries++;
ctx->orderly = 0;
(void)FWTPM_NV_SaveFlags(ctx);
}
if (ctx->daFailedTries >= ctx->daMaxTries) {
return 1;
}
}
return 0;
}
#endif /* !FWTPM_NO_DA */
/* ================================================================== */
@ -16093,8 +16302,32 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx,
* Per TPM 2.0 spec, certain commands must still be allowed during lockout
* (e.g., GetCapability, DictionaryAttackLockReset for recovery). */
#ifndef FWTPM_NO_DA
/* Self-heal first so elapsed time can clear lockout before the gate. */
FwDaSelfHeal(ctx);
/* A failed lockoutAuth locks the lockout hierarchy itself for
* daLockoutRecovery seconds, so lockoutAuth cannot be brute-forced
* (Part 1 Sec.19.8.2). Reject any command that authorizes via lockoutAuth
* (auth handle == TPM_RH_LOCKOUT: LockReset, Parameters, Clear/lockout,
* etc.) but not Clear via platformAuth, which is the recovery path. */
if (ctx->lockoutAuthFailed && entry->authHandleCnt > 0 &&
cmdHandleCnt > 0 && cmdHandles[0] == TPM_RH_LOCKOUT) {
*rspSize = FwBuildErrorResponse(rspBuf,
TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT);
return TPM_RC_SUCCESS;
}
if (ctx->daFailedTries >= ctx->daMaxTries && ctx->daMaxTries > 0) {
if (cmdCode != TPM_CC_GetCapability &&
/* Startup/Shutdown are lifecycle commands and must never be DA-gated:
* with failedTries persisted, gating Startup would brick a TPM that
* power-cycled while in lockout (Startup blocked, so LockReset can
* never run). Clear is the platform/lockout-authorized recovery escape
* hatch and is likewise exempt (DA protects entity auth, not the
* platform hierarchy). */
if (cmdCode != TPM_CC_Startup &&
cmdCode != TPM_CC_Shutdown &&
cmdCode != TPM_CC_Clear &&
cmdCode != TPM_CC_GetCapability &&
cmdCode != TPM_CC_SelfTest &&
cmdCode != TPM_CC_GetRandom &&
cmdCode != TPM_CC_DictionaryAttackLockReset &&
@ -16107,6 +16340,39 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx,
return TPM_RC_SUCCESS;
}
}
/* daUsed: the first use of a DA-protected (non-noDA) object/NV auth after
* startup must persist that DA tracking is armed, so a later power loss is
* penalized (Part 1 Sec.19.8.5). A real TPM returns TPM_RC_RETRY while it
* writes NV; emulate that under FWTPM_DA_USED_RETRY so callers exercise
* resubmit. The flag/orderly-state is persisted regardless of the macro. */
if (!ctx->daUsed) {
int armDa = 0;
for (pj = 0; pj < cmdAuthCnt && pj < (int)entry->authHandleCnt; pj++) {
TPM_HANDLE eH = cmdHandles[pj];
int isObj =
(eH & 0xFF000000) == (TRANSIENT_FIRST & 0xFF000000) ||
(eH & 0xFF000000) == (PERSISTENT_FIRST & 0xFF000000) ||
(eH & 0xFF000000) == (NV_INDEX_FIRST & 0xFF000000);
if ((cmdAuths[pj].handle == TPM_RS_PW ||
(cmdAuths[pj].sess != NULL &&
cmdAuths[pj].sess->sessionType == TPM_SE_HMAC)) &&
isObj && !FwHandleIsNoDA(ctx, eH)) {
armDa = 1;
break;
}
}
if (armDa) {
ctx->daUsed = 1;
ctx->orderly = 0;
(void)FWTPM_NV_SaveFlags(ctx);
#ifdef FWTPM_DA_USED_RETRY
*rspSize = FwBuildErrorResponse(rspBuf,
TPM_ST_NO_SESSIONS, TPM_RC_RETRY);
return TPM_RC_SUCCESS;
#endif
}
}
#endif
/* userWithAuth enforcement: per TPM 2.0 spec Part 1, Section 19.7.1,
@ -16157,14 +16423,10 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx,
"0x%x (CC=0x%x)\n", entityH, cmdCode);
#endif
#ifndef FWTPM_NO_DA
/* A failed auth against a NO_DA index must not feed lockout */
if (!FwHandleIsNoDA(ctx, entityH)) {
ctx->daFailedTries++;
if (ctx->daFailedTries >= ctx->daMaxTries) {
*rspSize = FwBuildErrorResponse(rspBuf,
TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT);
return TPM_RC_SUCCESS;
}
if (FwDaRegisterFailure(ctx, entityH)) {
*rspSize = FwBuildErrorResponse(rspBuf,
TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT);
return TPM_RC_SUCCESS;
}
#endif
*rspSize = FwBuildErrorResponse(rspBuf,
@ -16254,14 +16516,10 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx,
"0x%x (CC=0x%x)\n", entityH, cmdCode);
#endif
#ifndef FWTPM_NO_DA
/* A failed auth against a NO_DA index must not feed lockout */
if (!FwHandleIsNoDA(ctx, entityH)) {
ctx->daFailedTries++;
if (ctx->daFailedTries >= ctx->daMaxTries) {
*rspSize = FwBuildErrorResponse(rspBuf,
TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT);
return TPM_RC_SUCCESS;
}
if (FwDaRegisterFailure(ctx, entityH)) {
*rspSize = FwBuildErrorResponse(rspBuf,
TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT);
return TPM_RC_SUCCESS;
}
#endif
*rspSize = FwBuildErrorResponse(rspBuf,

View File

@ -59,11 +59,6 @@
/* TLV header size: tag(2) + length(2) */
#define TLV_HDR_SIZE 4
/* Dictionary-attack bounds used to sanitize values replayed from the
* integrity-unprotected NV journal. */
#define FWTPM_DA_DEFAULT_MAX_TRIES 32
#define FWTPM_DA_MAX_TRIES_LIMIT 0xFFFF
/* ========================================================================= */
/* File-based NV backend */
/* ========================================================================= */
@ -1052,12 +1047,30 @@ static int FwNvProcessEntry(FWTPM_CTX* ctx, UINT16 tag,
ctx->daMaxTries > FWTPM_DA_MAX_TRIES_LIMIT) {
ctx->daMaxTries = FWTPM_DA_DEFAULT_MAX_TRIES;
}
ctx->daFailedTries = 0; /* volatile - reset on load */
ctx->daUsed = 0; /* per-boot, reset on load */
#endif
/* resetCount trails the DA fields in newer journals */
if (vPos + 4 <= vMax) {
FwNvUnmarshalU32(value, &vPos, vMax, &ctx->resetCount);
}
#ifndef FWTPM_NO_DA
/* daFailedTries trails resetCount in journals that record it.
* Older journals lack it: treat as a clean orderly checkpoint so
* an upgrade does not trigger a spurious lockout penalty. */
if (vPos + 4 <= vMax) {
FwNvUnmarshalU32(value, &vPos, vMax, &ctx->daFailedTries);
if (ctx->daFailedTries > FWTPM_DA_MAX_TRIES_LIMIT) {
ctx->daFailedTries = ctx->daMaxTries;
}
ctx->orderly = (flags8 & 0x04) ? 1 : 0;
ctx->lockoutAuthFailed = (flags8 & 0x08) ? 1 : 0;
}
else {
ctx->daFailedTries = 0;
ctx->orderly = 1;
ctx->lockoutAuthFailed = 0;
}
#endif
break;
}
@ -1414,10 +1427,12 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx)
#ifndef FWTPM_NO_DA
/* DA protection defaults */
ctx->daMaxTries = 32;
ctx->daRecoveryTime = 600;
ctx->daLockoutRecovery = 86400;
ctx->daMaxTries = FWTPM_DA_DEFAULT_MAX_TRIES;
ctx->daRecoveryTime = FWTPM_DA_DEFAULT_RECOVERY;
ctx->daLockoutRecovery = FWTPM_DA_DEFAULT_LOCKOUT_RECOVERY;
ctx->daFailedTries = 0;
ctx->daUsed = 0;
ctx->orderly = 1;
#endif
/* PCR auth/policy defaults */
@ -1580,13 +1595,21 @@ int FWTPM_NV_Save(FWTPM_CTX* ctx)
pos = 0;
FwNvMarshalU8(buf, &pos, bufSz,
(UINT8)((ctx->disableClear ? 0x01 : 0x00) |
(ctx->globalNvWriteLock ? 0x02 : 0x00)));
(ctx->globalNvWriteLock ? 0x02 : 0x00)
#ifndef FWTPM_NO_DA
| (ctx->orderly ? 0x04 : 0x00)
| (ctx->lockoutAuthFailed ? 0x08 : 0x00)
#endif
));
#ifndef FWTPM_NO_DA
FwNvMarshalU32(buf, &pos, bufSz, ctx->daMaxTries);
FwNvMarshalU32(buf, &pos, bufSz, ctx->daRecoveryTime);
FwNvMarshalU32(buf, &pos, bufSz, ctx->daLockoutRecovery);
#endif
FwNvMarshalU32(buf, &pos, bufSz, ctx->resetCount);
#ifndef FWTPM_NO_DA
FwNvMarshalU32(buf, &pos, bufSz, ctx->daFailedTries);
#endif
rc = FwNvAppendEntry(ctx, FWTPM_NV_TAG_FLAGS, buf, (UINT16)pos);
}
@ -1867,7 +1890,7 @@ int FWTPM_NV_SaveFlags(FWTPM_CTX* ctx)
return TPM_RC_SUCCESS;
#else
int rc;
byte buf[1 + 12 + 4]; /* flags + DA params + resetCount */
byte buf[1 + 12 + 4 + 4]; /* flags + DA params + resetCount + failedTries */
word32 pos = 0;
if (ctx == NULL) {
@ -1876,13 +1899,21 @@ int FWTPM_NV_SaveFlags(FWTPM_CTX* ctx)
FwNvMarshalU8(buf, &pos, sizeof(buf),
(UINT8)((ctx->disableClear ? 0x01 : 0x00) |
(ctx->globalNvWriteLock ? 0x02 : 0x00)));
(ctx->globalNvWriteLock ? 0x02 : 0x00)
#ifndef FWTPM_NO_DA
| (ctx->orderly ? 0x04 : 0x00)
| (ctx->lockoutAuthFailed ? 0x08 : 0x00)
#endif
));
#ifndef FWTPM_NO_DA
FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daMaxTries);
FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daRecoveryTime);
FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daLockoutRecovery);
#endif
FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->resetCount);
#ifndef FWTPM_NO_DA
FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daFailedTries);
#endif
rc = FwNvAppendEntry(ctx, FWTPM_NV_TAG_FLAGS, buf, (UINT16)pos);
return rc;

View File

@ -7538,6 +7538,53 @@ int wolfTPM2_Clear(WOLFTPM2_DEV* dev)
return rc;
}
int wolfTPM2_DictionaryAttackLockReset(WOLFTPM2_DEV* dev)
{
int rc;
DictionaryAttackLockReset_In in;
if (dev == NULL)
return BAD_FUNC_ARG;
XMEMSET(&in, 0, sizeof(in));
in.lockHandle = TPM_RH_LOCKOUT;
rc = TPM2_DictionaryAttackLockReset(&in);
#ifdef DEBUG_WOLFTPM
if (rc != TPM_RC_SUCCESS) {
printf("TPM2_DictionaryAttackLockReset failed %d: %s\n", rc,
wolfTPM2_GetRCString(rc));
}
#endif
return rc;
}
int wolfTPM2_DictionaryAttackParameters(WOLFTPM2_DEV* dev,
word32 newMaxTries, word32 newRecoveryTime, word32 lockoutRecovery)
{
int rc;
DictionaryAttackParameters_In in;
/* newMaxTries of 0 would permanently disable lockout enforcement */
if (dev == NULL || newMaxTries == 0)
return BAD_FUNC_ARG;
XMEMSET(&in, 0, sizeof(in));
in.lockHandle = TPM_RH_LOCKOUT;
in.newMaxTries = newMaxTries;
in.newRecoveryTime = newRecoveryTime;
in.lockoutRecovery = lockoutRecovery;
rc = TPM2_DictionaryAttackParameters(&in);
#ifdef DEBUG_WOLFTPM
if (rc != TPM_RC_SUCCESS) {
printf("TPM2_DictionaryAttackParameters failed %d: %s\n", rc,
wolfTPM2_GetRCString(rc));
}
#endif
return rc;
}
/* Hashing */
/* usageAuth: Optional auth for handle */
int wolfTPM2_HashStart(WOLFTPM2_DEV* dev, WOLFTPM2_HASH* hash,

View File

@ -0,0 +1,94 @@
#!/bin/bash
#
# fwtpm_da_retry.sh - isolated end-to-end check of the fwTPM Dictionary Attack
# TPM_RC_RETRY (daUsed) path.
#
# Requires a build with -DFWTPM_DA_USED_RETRY: the fwTPM server then returns
# TPM_RC_RETRY on the first DA-protected (non-noDA) auth use while it persists
# the daUsed flag. That breaks non-retry-aware clients, so this harness starts
# the server and runs ONLY examples/management/da_check -lockout, which rides
# through RC_RETRY, drives lockout, and recovers.
#
# Exit: 0 = pass, 77 = skip (not built / wrong build), non-zero = fail
#
BUILD_DIR="$(pwd)"
SERVER="$BUILD_DIR/src/fwtpm/fwtpm_server"
DA_CHECK="$BUILD_DIR/examples/management/da_check"
OPTIONS="$BUILD_DIR/wolftpm/options.h"
OUT="/tmp/fwtpm_da_retry_$$.out"
PID=""
cleanup() {
[ -n "$PID" ] && kill "$PID" 2>/dev/null
rm -f "$OUT" "/tmp/fwtpm_da_srv_$$.log"
}
trap cleanup EXIT
if [ ! -x "$SERVER" ] || [ ! -x "$DA_CHECK" ]; then
echo "SKIP: fwtpm_server or da_check not built"
exit 77
fi
# The RC_RETRY emulation only exists when built with -DFWTPM_DA_USED_RETRY.
if [ ! -f "$OPTIONS" ] || \
! grep -q "^#define FWTPM_DA_USED_RETRY" "$OPTIONS"; then
echo "SKIP: build lacks FWTPM_DA_USED_RETRY"
exit 77
fi
IS_SWTPM=0
if [ -f "$OPTIONS" ] && grep -q "^#define WOLFTPM_SWTPM" "$OPTIONS"; then
IS_SWTPM=1
fi
rm -f "$BUILD_DIR/fwtpm_nv.bin" /tmp/fwtpm.shm
if [ $IS_SWTPM -eq 1 ]; then
PORT="${TPM2_SWTPM_PORT:-2371}"
"$SERVER" --clear --port "$PORT" --platform-port "$((PORT + 1))" \
>/tmp/fwtpm_da_srv_$$.log 2>&1 &
PID=$!
export TPM2_SWTPM_PORT="$PORT"
for i in $(seq 1 500); do
if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then
exec 3>&- 3<&-
break
fi
sleep 0.01
done
else
"$SERVER" --clear >/tmp/fwtpm_da_srv_$$.log 2>&1 &
PID=$!
elapsed=0
while [ ! -e /tmp/fwtpm.shm ] && [ $elapsed -lt 500 ]; do
sleep 0.01
elapsed=$((elapsed + 1))
done
fi
if ! kill -0 "$PID" 2>/dev/null; then
echo "FAIL: fwtpm_server failed to start"
cat /tmp/fwtpm_da_srv_$$.log 2>/dev/null
exit 1
fi
"$DA_CHECK" -lockout >"$OUT" 2>&1
RESULT=$?
cat "$OUT"
if [ $RESULT -ne 0 ]; then
echo "FAIL: da_check -lockout exited $RESULT"
exit 1
fi
if ! grep -q "resubmitting" "$OUT"; then
echo "FAIL: expected TPM_RC_RETRY resubmit was not observed"
exit 1
fi
if ! grep -q "Recovered via DictionaryAttackLockReset" "$OUT"; then
echo "FAIL: lockout recovery path did not run"
exit 1
fi
echo "PASS: fwTPM DA RC_RETRY + lockout end-to-end"
exit 0

View File

@ -8423,6 +8423,18 @@ static void test_fwtpm_change_pps(void)
fwtpm_pass("ChangePPS:", 0);
}
/* Mock clock HAL state (shared by DA self-heal and clock HAL tests) */
static UINT64 gMockClockMs;
static int gMockClockCalls;
static void* gMockClockCtxSeen;
static UINT64 mock_clock_get_ms(void* ctx)
{
gMockClockCalls++;
gMockClockCtxSeen = ctx;
return gMockClockMs;
}
#ifndef FWTPM_NO_DA
static void test_fwtpm_da_parameters_and_reset(void)
{
@ -8478,6 +8490,755 @@ static void test_fwtpm_da_parameters_zero_maxtries_rejected(void)
FWTPM_Cleanup(&ctx);
printf("Test fwTPM:\tDA Parameters maxTries=0 rejected:\tPassed\n");
}
/* Query a single TPM property via GetCapability and return its value. */
static UINT32 DaGetCapProp(FWTPM_CTX* ctx, UINT32 prop)
{
int cmdSz, rspSize = 0;
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_GetCapability);
PutU32BE(gCmd + cmdSz, TPM_CAP_TPM_PROPERTIES); cmdSz += 4;
PutU32BE(gCmd + cmdSz, prop); cmdSz += 4;
PutU32BE(gCmd + cmdSz, 1); cmdSz += 4;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
/* hdr(10)+moreData(1)+cap(4)+count(4)=19: prop@19, value@23. Assert the
* returned tag matches so an absent property fails loudly rather than
* silently returning the next property's value. */
AssertIntEQ((int)GetU32BE(gRsp + 19), (int)prop);
return GetU32BE(gRsp + 23);
}
/* Set DA parameters (lockoutAuth empty) so tests run with short thresholds. */
static TPM_RC DaSetParams(FWTPM_CTX* ctx, UINT32 maxTries, UINT32 recovery,
UINT32 lockoutRecovery)
{
int pos = 0, rspSize = 0;
PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2;
PutU32BE(gCmd + pos, 0); pos += 4;
PutU32BE(gCmd + pos, TPM_CC_DictionaryAttackParameters); pos += 4;
PutU32BE(gCmd + pos, TPM_RH_LOCKOUT); pos += 4;
pos = AppendPwAuth(gCmd, pos, NULL, 0);
PutU32BE(gCmd + pos, maxTries); pos += 4;
PutU32BE(gCmd + pos, recovery); pos += 4;
PutU32BE(gCmd + pos, lockoutRecovery); pos += 4;
PutU32BE(gCmd + 2, (UINT32)pos);
FWTPM_ProcessCommand(ctx, gCmd, pos, gRsp, &rspSize, 0);
return GetRspRC(gRsp);
}
/* GetCapability reports the DA parameters and lockout state. */
static void test_fwtpm_da_getcap_properties(void)
{
FWTPM_CTX ctx;
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ(DaSetParams(&ctx, 5, 120, 600), TPM_RC_SUCCESS);
AssertIntEQ((int)DaGetCapProp(&ctx, TPM_PT_MAX_AUTH_FAIL), 5);
AssertIntEQ((int)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_INTERVAL), 120);
AssertIntEQ((int)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_RECOVERY), 600);
AssertIntEQ((int)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER), 0);
AssertFalse(DaGetCapProp(&ctx, TPM_PT_PERMANENT) &
TPMA_PERMANENT_inLockout);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA GetCapability properties:", 0);
}
/* Send a command with a wrong (non-empty) password to a given handle. */
static TPM_RC DaSendBadAuthTo(FWTPM_CTX* ctx, UINT32 cc, UINT32 handle)
{
int pos = 0, rspSize = 0;
static const byte badPw[] = { 'b', 'a', 'd' };
PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2;
PutU32BE(gCmd + pos, 0); pos += 4;
PutU32BE(gCmd + pos, cc); pos += 4;
PutU32BE(gCmd + pos, handle); pos += 4;
pos = AppendPwAuth(gCmd, pos, badPw, (int)sizeof(badPw));
PutU32BE(gCmd + 2, (UINT32)pos);
FWTPM_ProcessCommand(ctx, gCmd, pos, gRsp, &rspSize, 0);
return GetRspRC(gRsp);
}
/* The platform hierarchy is not DA-protected: a failed platformAuth reports
* AUTH_FAIL (not LOCKOUT) and never feeds the counter. */
static void test_fwtpm_da_platform_exempt(void)
{
FWTPM_CTX ctx;
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ(DaSendBadAuthTo(&ctx, TPM_CC_Clear, TPM_RH_PLATFORM),
TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 0);
/* Repeated failures must neither count nor lock. */
AssertIntEQ(DaSendBadAuthTo(&ctx, TPM_CC_Clear, TPM_RH_PLATFORM),
TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 0);
AssertIntEQ(ctx.lockoutAuthFailed, 0);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA platform hierarchy exempt:", 0);
}
/* Startup(SU_STATE) exercises the Restart/Resume DA-flag persist branch. */
static void test_fwtpm_da_startup_state(void)
{
FWTPM_CTX ctx;
int rspSize = 0, cmdSz;
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(FWTPM_Init(&ctx), 0);
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_STATE); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
AssertIntEQ((int)ctx.restartCount, 1);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA Startup(SU_STATE) persist:", 0);
}
#ifdef HAVE_ECC
/* Create an ECC-P256 sign primary in the owner hierarchy with empty auth and
* the caller-supplied object attributes. Returns the transient handle. */
static UINT32 DaMakeEccSignKey(FWTPM_CTX* ctx, UINT32 attributes)
{
int pos = 0, pubStart, pubLen, sensStart, sensLen, rspSize = 0;
PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2;
PutU32BE(gCmd + pos, 0); pos += 4;
PutU32BE(gCmd + pos, TPM_CC_CreatePrimary); pos += 4;
PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4;
PutU32BE(gCmd + pos, 9); pos += 4;
PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4;
PutU16BE(gCmd + pos, 0); pos += 2;
gCmd[pos++] = 0;
PutU16BE(gCmd + pos, 0); pos += 2;
sensStart = pos;
PutU16BE(gCmd + pos, 0); pos += 2;
PutU16BE(gCmd + pos, 0); pos += 2;
PutU16BE(gCmd + pos, 0); pos += 2;
sensLen = pos - sensStart - 2;
PutU16BE(gCmd + sensStart, (UINT16)sensLen);
pubStart = pos;
PutU16BE(gCmd + pos, 0); pos += 2;
PutU16BE(gCmd + pos, TPM_ALG_ECC); pos += 2;
PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2;
PutU32BE(gCmd + pos, attributes); pos += 4;
PutU16BE(gCmd + pos, 0); pos += 2;
PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2;
PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2;
PutU16BE(gCmd + pos, TPM_ECC_NIST_P256); pos += 2;
PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2;
PutU16BE(gCmd + pos, 0); pos += 2;
PutU16BE(gCmd + pos, 0); pos += 2;
pubLen = pos - pubStart - 2;
PutU16BE(gCmd + pubStart, (UINT16)pubLen);
PutU16BE(gCmd + pos, 0); pos += 2;
PutU32BE(gCmd + pos, 0); pos += 4;
PutU32BE(gCmd + 2, (UINT32)pos);
FWTPM_ProcessCommand(ctx, gCmd, pos, gRsp, &rspSize, 0);
if (GetRspRC(gRsp) != TPM_RC_SUCCESS) return 0;
return GetU32BE(gRsp + TPM2_HEADER_SIZE);
}
/* Send a Sign command with a deliberately wrong password. Authorization is
* checked before the handler, so this fails at auth (driving DA accounting)
* without needing well-formed sign parameters. */
static TPM_RC DaSignWrongAuth(FWTPM_CTX* ctx, UINT32 keyH)
{
int pos = 0, rspSize = 0;
static const byte badPw[] = { 'b', 'a', 'd' };
PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2;
PutU32BE(gCmd + pos, 0); pos += 4;
PutU32BE(gCmd + pos, TPM_CC_Sign); pos += 4;
PutU32BE(gCmd + pos, keyH); pos += 4;
pos = AppendPwAuth(gCmd, pos, badPw, (int)sizeof(badPw));
PutU32BE(gCmd + 2, (UINT32)pos);
FWTPM_ProcessCommand(ctx, gCmd, pos, gRsp, &rspSize, 0);
return GetRspRC(gRsp);
}
/* A noDA object must never feed the lockout counter; a DA-protected object
* must. This exercises the FwHandleIsNoDA object path (the noDA-on-objects
* fix). */
static void test_fwtpm_da_noda_object_exempt(void)
{
FWTPM_CTX ctx;
UINT32 daKey, noDaKey;
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
/* noDA sign key (attr includes TPMA_OBJECT_noDA = 0x400) */
noDaKey = DaMakeEccSignKey(&ctx, 0x00040472);
AssertIntNE(noDaKey, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, noDaKey), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 0);
AssertIntEQ(ctx.daUsed, 0);
/* DA-protected sign key (no noDA bit) */
daKey = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(daKey, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, daKey), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 1);
AssertIntEQ(ctx.daUsed, 1);
/* Setting DA parameters also resets a non-zero counter (TCG Part 3). */
AssertIntEQ(DaSetParams(&ctx, 5, 0, 0), TPM_RC_SUCCESS);
AssertIntEQ((int)ctx.daFailedTries, 0);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA noDA object exempt:", 0);
}
/* Failed auth drives lockout; LockReset recovers. */
static void test_fwtpm_da_lockout_and_reset(void)
{
FWTPM_CTX ctx;
UINT32 key;
int rspSize = 0, cmdSz;
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ(DaSetParams(&ctx, 2, 0, 0), TPM_RC_SUCCESS);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER), 1);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_LOCKOUT);
AssertIntEQ((int)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER), 2);
AssertTrue(DaGetCapProp(&ctx, TPM_PT_PERMANENT) &
TPMA_PERMANENT_inLockout);
/* A non-whitelisted command is now rejected with LOCKOUT. */
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_LOCKOUT);
/* Lifecycle commands are exempt: Shutdown works during lockout. */
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Shutdown);
PutU16BE(gCmd + cmdSz, TPM_SU_STATE); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
/* DictionaryAttackLockReset clears the counter. */
AssertIntEQ(SendSimpleSessionCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
TPM_RH_LOCKOUT), TPM_RC_SUCCESS);
AssertIntEQ((int)ctx.daFailedTries, 0);
AssertIntEQ((int)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER), 0);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA lockout + LockReset:", 0);
}
/* A noDA key stays usable during active lockout (gate exempts it): a wrong-auth
* use yields AUTH_FAIL, not LOCKOUT. */
static void test_fwtpm_da_noda_usable_during_lockout(void)
{
FWTPM_CTX ctx;
UINT32 daKey, noDaKey;
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ(DaSetParams(&ctx, 2, 0, 0), TPM_RC_SUCCESS);
noDaKey = DaMakeEccSignKey(&ctx, 0x00040472); /* noDA set */
daKey = DaMakeEccSignKey(&ctx, 0x00040072); /* noDA clear */
AssertIntNE(noDaKey, 0);
AssertIntNE(daKey, 0);
/* Drive lockout with the DA key. */
AssertIntEQ(DaSignWrongAuth(&ctx, daKey), TPM_RC_AUTH_FAIL);
AssertIntEQ(DaSignWrongAuth(&ctx, daKey), TPM_RC_LOCKOUT);
/* The DA key is now gated... */
AssertIntEQ(DaSignWrongAuth(&ctx, daKey), TPM_RC_LOCKOUT);
/* ...but the noDA key is still processed (AUTH_FAIL, not LOCKOUT) and does
* not feed the counter. */
AssertIntEQ(DaSignWrongAuth(&ctx, noDaKey), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 2);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA noDA usable during lockout:", 0);
}
/* failedTries self-heals as recoveryTime elapses (clock HAL injected). */
static void test_fwtpm_da_selfheal(void)
{
FWTPM_CTX ctx;
UINT32 key;
int rspSize = 0, cmdSz;
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(FWTPM_Init(&ctx), 0);
gMockClockMs = 0;
AssertIntEQ(FWTPM_Clock_SetHAL(&ctx, mock_clock_get_ms, NULL), 0);
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
AssertIntEQ(DaSetParams(&ctx, 10, 1 /*sec*/, 0), TPM_RC_SUCCESS);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 2);
/* Advance 3s (>= 2 recovery intervals) and run a benign command. */
gMockClockMs = 3000;
(void)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER);
AssertIntEQ((int)ctx.daFailedTries, 0);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA self-heal:", 0);
}
#ifndef FWTPM_NO_NV
/* failedTries persists across reload; a non-orderly restart adds the +1
* penalty, an orderly Shutdown does not. */
static void test_fwtpm_da_persist_failedtries(void)
{
FWTPM_CTX ctx;
UINT32 key;
int rspSize = 0, cmdSz;
(void)remove(FWTPM_NV_FILE);
/* Boot 1: two failures, then a clean Shutdown (orderly). */
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ(DaSetParams(&ctx, 100, 0, 0), TPM_RC_SUCCESS);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 2);
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Shutdown);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
FWTPM_Cleanup(&ctx);
/* Boot 2: orderly shutdown means no penalty, counter persists at 2. */
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ((int)ctx.daFailedTries, 2);
/* One more failure, then drop power without Shutdown (non-orderly). */
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 3);
FWTPM_Cleanup(&ctx);
/* Boot 3: counter persists across the non-orderly reload. With no clock
* HAL the +1 non-orderly penalty is not charged (clockless targets cannot
* self-heal, so they must not accumulate). */
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ((int)ctx.daFailedTries, 3);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA persist failedTries:", 0);
}
/* With a clock HAL, a non-orderly restart charges the +1 penalty (and the
* lockoutAuth lock survives a reboot when lockoutRecovery != 0). */
static void test_fwtpm_da_nonorderly_penalty(void)
{
FWTPM_CTX ctx;
UINT32 key;
int rspSize = 0, cmdSz;
(void)remove(FWTPM_NV_FILE);
/* Boot 1 (clock present): one failure, then non-orderly power loss. */
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(FWTPM_Init(&ctx), 0);
gMockClockMs = 0;
AssertIntEQ(FWTPM_Clock_SetHAL(&ctx, mock_clock_get_ms, NULL), 0);
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
AssertIntEQ(DaSetParams(&ctx, 100, 0, 0), TPM_RC_SUCCESS);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 1);
FWTPM_Cleanup(&ctx);
/* Boot 2 (clock present): non-orderly restart adds the +1 penalty -> 2. */
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(FWTPM_Init(&ctx), 0);
gMockClockMs = 0;
AssertIntEQ(FWTPM_Clock_SetHAL(&ctx, mock_clock_get_ms, NULL), 0);
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
AssertIntEQ((int)ctx.daFailedTries, 2);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA non-orderly penalty (clocked):", 0);
}
/* A power-cycle while in lockout must not brick the TPM: Startup must still be
* accepted (not DA-gated) and DictionaryAttackLockReset must recover. */
static void test_fwtpm_da_reboot_in_lockout_recovers(void)
{
FWTPM_CTX ctx;
UINT32 key;
(void)remove(FWTPM_NV_FILE);
/* Boot 1: drive into lockout (maxTries=2), then drop power (non-orderly). */
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ(DaSetParams(&ctx, 2, 0, 0), TPM_RC_SUCCESS);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_LOCKOUT);
AssertTrue(ctx.daFailedTries >= ctx.daMaxTries);
FWTPM_Cleanup(&ctx);
/* Boot 2: Startup must succeed despite the persisted lockout, and
* DictionaryAttackLockReset must recover. */
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertTrue(ctx.daFailedTries >= ctx.daMaxTries);
AssertIntEQ(SendSimpleSessionCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
TPM_RH_LOCKOUT), TPM_RC_SUCCESS);
AssertIntEQ((int)ctx.daFailedTries, 0);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA reboot-in-lockout recovers:", 0);
}
#endif /* !FWTPM_NO_NV */
/* Send a lockout-hierarchy command (LockReset / Parameters) with a given
* password. Parameters carries three U32 args. */
static TPM_RC DaSendLockoutCmd(FWTPM_CTX* ctx, UINT32 cc,
const byte* pw, int pwSz)
{
int pos = 0, rspSize = 0;
PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2;
PutU32BE(gCmd + pos, 0); pos += 4;
PutU32BE(gCmd + pos, cc); pos += 4;
PutU32BE(gCmd + pos, TPM_RH_LOCKOUT); pos += 4;
pos = AppendPwAuth(gCmd, pos, pw, pwSz);
if (cc == TPM_CC_DictionaryAttackParameters) {
PutU32BE(gCmd + pos, 32); pos += 4; /* newMaxTries */
PutU32BE(gCmd + pos, 0); pos += 4; /* newRecoveryTime */
PutU32BE(gCmd + pos, 0); pos += 4; /* lockoutRecovery */
}
PutU32BE(gCmd + 2, (UINT32)pos);
FWTPM_ProcessCommand(ctx, gCmd, pos, gRsp, &rspSize, 0);
return GetRspRC(gRsp);
}
/* A failed lockoutAuth locks the lockout hierarchy: DA reset/parameters are
* then rejected even with the correct auth, until lockoutRecovery elapses. */
static void test_fwtpm_da_lockoutauth_failure(void)
{
FWTPM_CTX ctx;
int rspSize = 0, cmdSz;
static const byte lkAuth[] = { 'l', 'k', 'a' };
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(FWTPM_Init(&ctx), 0);
gMockClockMs = 0;
AssertIntEQ(FWTPM_Clock_SetHAL(&ctx, mock_clock_get_ms, NULL), 0);
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
/* Establish a non-empty lockoutAuth and a short lockoutRecovery. */
ctx.lockoutAuth.size = (UINT16)sizeof(lkAuth);
memcpy(ctx.lockoutAuth.buffer, lkAuth, sizeof(lkAuth));
ctx.daLockoutRecovery = 1; /* second */
/* The first wrong lockoutAuth reports AUTH_FAIL but arms the lock. */
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
NULL, 0), TPM_RC_AUTH_FAIL);
AssertIntEQ(ctx.lockoutAuthFailed, 1);
/* Even the correct auth is now rejected while the lockout lock holds. */
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackParameters,
lkAuth, (int)sizeof(lkAuth)), TPM_RC_LOCKOUT);
/* Brute-force via lockoutAuth-authorized Clear is also rate-limited (not an
* unlimited stream of AUTH_FAIL). */
AssertIntEQ(DaSendBadAuthTo(&ctx, TPM_CC_Clear, TPM_RH_LOCKOUT),
TPM_RC_LOCKOUT);
/* Advance past lockoutRecovery; a benign command self-heals the lock. */
gMockClockMs = 2000;
(void)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER);
AssertIntEQ(ctx.lockoutAuthFailed, 0);
/* Correct auth works again. */
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
lkAuth, (int)sizeof(lkAuth)), TPM_RC_SUCCESS);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA lockoutAuth lock + recovery:", 0);
}
#ifndef FWTPM_NO_NV
/* Init + optional clock HAL + Startup(CLEAR). Returns the Startup RC. */
static int DaManualStartup(FWTPM_CTX* ctx, int withClock)
{
int rspSize = 0, cmdSz;
if (FWTPM_Init(ctx) != 0) {
return -1;
}
if (withClock) {
gMockClockMs = 0;
if (FWTPM_Clock_SetHAL(ctx, mock_clock_get_ms, NULL) != 0) {
return -1;
}
}
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
return (int)GetRspRC(gRsp);
}
/* The lockoutAuth lock survives a reboot when a clock HAL is present and
* lockoutRecovery != 0, but a clockless reboot clears it (so clockless targets
* keep a reboot recovery path). */
static void test_fwtpm_da_lockoutauth_persist(void)
{
FWTPM_CTX ctx;
static const byte lkAuth[] = { 'l', 'k', 'a' };
/* Scenario A: clock present, default lockoutRecovery (>0) -> persists. */
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(DaManualStartup(&ctx, 1), TPM_RC_SUCCESS);
ctx.lockoutAuth.size = (UINT16)sizeof(lkAuth);
memcpy(ctx.lockoutAuth.buffer, lkAuth, sizeof(lkAuth));
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
NULL, 0), TPM_RC_AUTH_FAIL);
AssertIntEQ(ctx.lockoutAuthFailed, 1);
FWTPM_Cleanup(&ctx);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(DaManualStartup(&ctx, 1), TPM_RC_SUCCESS);
AssertIntEQ(ctx.lockoutAuthFailed, 1);
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
NULL, 0), TPM_RC_LOCKOUT);
FWTPM_Cleanup(&ctx);
/* Scenario B: clockless reboot clears the lock. */
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(DaManualStartup(&ctx, 0), TPM_RC_SUCCESS);
ctx.lockoutAuth.size = (UINT16)sizeof(lkAuth);
memcpy(ctx.lockoutAuth.buffer, lkAuth, sizeof(lkAuth));
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
NULL, 0), TPM_RC_AUTH_FAIL);
AssertIntEQ(ctx.lockoutAuthFailed, 1);
FWTPM_Cleanup(&ctx);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(DaManualStartup(&ctx, 0), TPM_RC_SUCCESS);
AssertIntEQ(ctx.lockoutAuthFailed, 0);
/* The lock cleared on the clockless reboot, so a correctly-authorized
* reset now succeeds (lockoutAuth persisted via the Cleanup full save). */
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
lkAuth, (int)sizeof(lkAuth)), TPM_RC_SUCCESS);
FWTPM_Cleanup(&ctx);
/* Scenario C: clock present but lockoutRecovery == 0 -> reboot clears. */
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(DaManualStartup(&ctx, 1), TPM_RC_SUCCESS);
ctx.daLockoutRecovery = 0;
ctx.lockoutAuth.size = (UINT16)sizeof(lkAuth);
memcpy(ctx.lockoutAuth.buffer, lkAuth, sizeof(lkAuth));
AssertIntEQ(DaSendLockoutCmd(&ctx, TPM_CC_DictionaryAttackLockReset,
NULL, 0), TPM_RC_AUTH_FAIL);
AssertIntEQ(ctx.lockoutAuthFailed, 1);
FWTPM_Cleanup(&ctx);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(DaManualStartup(&ctx, 1), TPM_RC_SUCCESS);
AssertIntEQ(ctx.lockoutAuthFailed, 0);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA lockoutAuth persists across reboot:", 0);
}
#endif /* !FWTPM_NO_NV */
/* TPM2_Clear restores DA state (counter and parameters) to defaults. */
static void test_fwtpm_da_clear_resets(void)
{
FWTPM_CTX ctx;
UINT32 key;
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
AssertIntEQ(DaSetParams(&ctx, 2, 30, 300), TPM_RC_SUCCESS);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
/* Drive into full lockout so the inLockout bit is set. */
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_LOCKOUT);
AssertTrue(DaGetCapProp(&ctx, TPM_PT_PERMANENT) &
TPMA_PERMANENT_inLockout);
/* Clear is allowed during lockout and resets DA state to defaults. */
AssertIntEQ(SendSimpleSessionCmd(&ctx, TPM_CC_Clear, TPM_RH_LOCKOUT),
TPM_RC_SUCCESS);
AssertIntEQ((int)ctx.daFailedTries, 0);
AssertIntEQ((int)ctx.daMaxTries, FWTPM_DA_DEFAULT_MAX_TRIES);
AssertIntEQ(ctx.orderly, 1);
AssertFalse(DaGetCapProp(&ctx, TPM_PT_PERMANENT) &
TPMA_PERMANENT_inLockout);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA Clear resets state:", 0);
}
/* TPM_PT_PERMANENT reports the auth-set and disableClear bits. */
static void test_fwtpm_da_permanent_bits(void)
{
FWTPM_CTX ctx;
UINT32 perm;
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
ctx.ownerAuth.size = 4;
ctx.endorsementAuth.size = 4;
ctx.lockoutAuth.size = 4;
ctx.disableClear = 1;
perm = DaGetCapProp(&ctx, TPM_PT_PERMANENT);
AssertTrue(perm & TPMA_PERMANENT_ownerAuthSet);
AssertTrue(perm & TPMA_PERMANENT_endorsementAuthSet);
AssertTrue(perm & TPMA_PERMANENT_lockoutAuthSet);
AssertTrue(perm & TPMA_PERMANENT_disableClear);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA PERMANENT bits:", 0);
}
/* The partial self-heal branch forgives some-but-not-all tries and carries the
* residual baseline forward. */
static void test_fwtpm_da_selfheal_partial(void)
{
FWTPM_CTX ctx;
UINT32 key;
int rspSize = 0, cmdSz;
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(FWTPM_Init(&ctx), 0);
gMockClockMs = 0;
AssertIntEQ(FWTPM_Clock_SetHAL(&ctx, mock_clock_get_ms, NULL), 0);
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
AssertIntEQ(DaSetParams(&ctx, 10, 1 /*sec*/, 0), TPM_RC_SUCCESS);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ(DaSignWrongAuth(&ctx, key), TPM_RC_AUTH_FAIL);
AssertIntEQ((int)ctx.daFailedTries, 3);
/* One recovery interval forgives exactly one try (partial). */
gMockClockMs = 1000;
(void)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER);
AssertIntEQ((int)ctx.daFailedTries, 2);
/* The next interval forgives one more, proving the baseline carried. */
gMockClockMs = 2000;
(void)DaGetCapProp(&ctx, TPM_PT_LOCKOUT_COUNTER);
AssertIntEQ((int)ctx.daFailedTries, 1);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA self-heal partial:", 0);
}
/* An HMAC session authorizing a DA-protected object also arms daUsed (arming
* runs before HMAC verification). */
static void test_fwtpm_da_hmac_arms_daused(void)
{
FWTPM_CTX ctx;
UINT32 key, sessH;
int pos = 0, authStart, rspSize = 0;
(void)remove(FWTPM_NV_FILE);
memset(&ctx, 0, sizeof(ctx));
AssertIntEQ(fwtpm_test_startup(&ctx), 0);
key = DaMakeEccSignKey(&ctx, 0x00040072);
AssertIntNE(key, 0);
sessH = StartSessionHelper(&ctx, TPM_SE_HMAC);
AssertIntNE(sessH, 0);
AssertIntEQ(ctx.daUsed, 0);
/* Sign authorized by the HMAC session (garbage HMAC). The auth area is
* well-formed so the session resolves and arms daUsed; verification then
* fails. */
PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2;
PutU32BE(gCmd + pos, 0); pos += 4;
PutU32BE(gCmd + pos, TPM_CC_Sign); pos += 4;
PutU32BE(gCmd + pos, key); pos += 4;
authStart = pos;
PutU32BE(gCmd + pos, 0); pos += 4; /* authSize placeholder */
PutU32BE(gCmd + pos, sessH); pos += 4; /* session handle */
PutU16BE(gCmd + pos, 16); pos += 2; /* nonceCaller size */
memset(gCmd + pos, 0xAA, 16); pos += 16;
gCmd[pos++] = 0x01; /* attributes: continueSession */
PutU16BE(gCmd + pos, 32); pos += 2; /* hmac size */
memset(gCmd + pos, 0xCC, 32); pos += 32; /* garbage hmac */
PutU32BE(gCmd + authStart, (UINT32)(pos - authStart - 4));
PutU32BE(gCmd + 2, (UINT32)pos);
FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_AUTH_FAIL);
AssertIntEQ(ctx.daUsed, 1);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA HMAC session arms daUsed:", 0);
}
#endif /* HAVE_ECC */
#endif /* !FWTPM_NO_DA */
static void test_fwtpm_read_public(void)
@ -9059,18 +9820,6 @@ static void test_fwtpm_clock_set(void)
/* HAL registration tests */
/* ================================================================== */
/* Mock clock HAL state */
static UINT64 gMockClockMs;
static int gMockClockCalls;
static void* gMockClockCtxSeen;
static UINT64 mock_clock_get_ms(void* ctx)
{
gMockClockCalls++;
gMockClockCtxSeen = ctx;
return gMockClockMs;
}
static void test_fwtpm_clock_sethal(void)
{
FWTPM_CTX ctx;
@ -9149,6 +9898,143 @@ static int mock_nv_erase(void* c, word32 off, word32 sz)
gMockNvErases++;
return TPM_RC_SUCCESS;
}
#ifndef FWTPM_NO_DA
static void PutU32LE(byte* p, UINT32 v)
{
p[0] = (byte)(v & 0xFF);
p[1] = (byte)((v >> 8) & 0xFF);
p[2] = (byte)((v >> 16) & 0xFF);
p[3] = (byte)((v >> 24) & 0xFF);
}
/* Build a minimal NV journal (header + one FLAGS entry) in gMockNvStore and
* load it through the mock HAL (no integrity key, so the MAC check is skipped).
* This lets the FLAGS reload paths be exercised with hand-crafted bytes. */
static void DaLoadCraftedFlags(FWTPM_CTX* ctx, const byte* flagsVal,
int flagsLen)
{
FWTPM_NV_HAL hal;
word32 writePos = 16 + 4 + (word32)flagsLen; /* header + TLV hdr + value */
memset(gMockNvStore, 0xFF, sizeof(gMockNvStore));
PutU32LE(gMockNvStore + 0, 0x66775450); /* magic "fwTP" */
PutU32LE(gMockNvStore + 4, 4); /* version (TLV journal) */
PutU32LE(gMockNvStore + 8, writePos);
PutU32LE(gMockNvStore + 12, MOCK_NV_SIZE);
gMockNvStore[16] = 0x30; gMockNvStore[17] = 0x00; /* FLAGS tag (LE) */
gMockNvStore[18] = (byte)(flagsLen & 0xFF);
gMockNvStore[19] = (byte)((flagsLen >> 8) & 0xFF);
memcpy(gMockNvStore + 20, flagsVal, (size_t)flagsLen);
memset(ctx, 0, sizeof(*ctx));
memset(&hal, 0, sizeof(hal)); /* get_integrity_key == NULL -> no MAC */
hal.read = mock_nv_read;
hal.write = mock_nv_write;
hal.erase = mock_nv_erase;
hal.maxSize = MOCK_NV_SIZE;
AssertIntEQ(FWTPM_NV_SetHAL(ctx, &hal), 0);
AssertIntEQ(FWTPM_Init(ctx), 0);
}
/* An older journal lacking the trailing daFailedTries field must load as a
* clean checkpoint (no spurious lockout penalty on upgrade). */
static void test_fwtpm_da_oldformat_journal(void)
{
FWTPM_CTX ctx;
byte val[17]; /* flags8 + maxTries + recoveryTime + lockoutRecovery + reset */
val[0] = 0x04; /* legacy flags: orderly bit set */
PutU32LE(val + 1, 32); /* daMaxTries */
PutU32LE(val + 5, 600); /* daRecoveryTime */
PutU32LE(val + 9, 86400); /* daLockoutRecovery */
PutU32LE(val + 13, 7); /* resetCount */
DaLoadCraftedFlags(&ctx, val, (int)sizeof(val));
AssertIntEQ((int)ctx.daFailedTries, 0);
AssertIntEQ(ctx.orderly, 1);
AssertIntEQ(ctx.lockoutAuthFailed, 0);
AssertIntEQ((int)ctx.resetCount, 7);
FWTPM_Cleanup(&ctx);
fwtpm_pass("DA old-format journal load:", 0);
}
/* A replayed/out-of-range daFailedTries is clamped on load. */
static void test_fwtpm_da_failedtries_clamp(void)
{
FWTPM_CTX ctx;
byte val[21]; /* old 17 bytes + daFailedTries */
val[0] = 0x04; /* orderly bit set */
PutU32LE(val + 1, 32); /* daMaxTries */
PutU32LE(val + 5, 600);
PutU32LE(val + 9, 86400);
PutU32LE(val + 13, 0); /* resetCount */
PutU32LE(val + 17, 0x00010000);/* daFailedTries = 65536 (> 0xFFFF) */
DaLoadCraftedFlags(&ctx, val, (int)sizeof(val));
AssertIntEQ((int)ctx.daFailedTries, (int)ctx.daMaxTries);
FWTPM_Cleanup(&ctx);
fwtpm_pass("DA failedTries clamp on load:", 0);
}
/* A persisted lockout must survive reload even when the clock source returns a
* large (RTC-like / clockOffset-advanced) value: self-heal must not measure
* elapsed time from an unseeded baseline and wipe the counter. */
static void test_fwtpm_da_no_selfheal_wipe_on_reload(void)
{
FWTPM_CTX ctx;
byte val[21];
int rspSize = 0, cmdSz;
val[0] = 0x04; /* orderly */
PutU32LE(val + 1, 32); /* daMaxTries */
PutU32LE(val + 5, 600); /* daRecoveryTime */
PutU32LE(val + 9, 86400); /* daLockoutRecovery */
PutU32LE(val + 13, 0); /* resetCount */
PutU32LE(val + 17, 32); /* daFailedTries = maxTries (locked) */
DaLoadCraftedFlags(&ctx, val, (int)sizeof(val));
AssertIntEQ((int)ctx.daFailedTries, 32);
/* Register a clock returning a large value, as an RTC or an advanced
* clockOffset would after reboot. */
gMockClockMs = 5000000;
AssertIntEQ(FWTPM_Clock_SetHAL(&ctx, mock_clock_get_ms, NULL), 0);
/* A pre-Startup GetCapability must not heal the loaded counter. */
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_GetCapability);
PutU32BE(gCmd + cmdSz, TPM_CAP_TPM_PROPERTIES); cmdSz += 4;
PutU32BE(gCmd + cmdSz, TPM_PT_LOCKOUT_COUNTER); cmdSz += 4;
PutU32BE(gCmd + cmdSz, 1); cmdSz += 4;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ((int)ctx.daFailedTries, 32);
/* Startup seeds the self-heal baseline; the lockout still holds. */
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup);
PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS);
AssertTrue(ctx.daFailedTries >= ctx.daMaxTries);
/* A post-Startup command at the same clock value must not wipe it. */
cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_GetCapability);
PutU32BE(gCmd + cmdSz, TPM_CAP_TPM_PROPERTIES); cmdSz += 4;
PutU32BE(gCmd + cmdSz, TPM_PT_LOCKOUT_COUNTER); cmdSz += 4;
PutU32BE(gCmd + cmdSz, 1); cmdSz += 4;
PutU32BE(gCmd + 2, (UINT32)cmdSz);
FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0);
AssertTrue(ctx.daFailedTries >= ctx.daMaxTries);
FWTPM_Cleanup(&ctx);
(void)remove(FWTPM_NV_FILE);
fwtpm_pass("DA no self-heal wipe on reload:", 0);
}
#endif /* !FWTPM_NO_DA */
#endif /* !FWTPM_NO_NV */
/* Register a mock NV HAL before FWTPM_Init and verify that NV writes
@ -9379,6 +10265,31 @@ int fwtpm_unit_tests(int argc, char *argv[])
#ifndef FWTPM_NO_DA
test_fwtpm_da_parameters_and_reset();
test_fwtpm_da_parameters_zero_maxtries_rejected();
test_fwtpm_da_getcap_properties();
test_fwtpm_da_platform_exempt();
test_fwtpm_da_startup_state();
#ifndef FWTPM_NO_NV
test_fwtpm_da_oldformat_journal();
test_fwtpm_da_failedtries_clamp();
test_fwtpm_da_no_selfheal_wipe_on_reload();
#endif
#ifdef HAVE_ECC
test_fwtpm_da_noda_object_exempt();
test_fwtpm_da_lockout_and_reset();
test_fwtpm_da_noda_usable_during_lockout();
test_fwtpm_da_selfheal();
test_fwtpm_da_selfheal_partial();
test_fwtpm_da_hmac_arms_daused();
test_fwtpm_da_lockoutauth_failure();
test_fwtpm_da_clear_resets();
test_fwtpm_da_permanent_bits();
#ifndef FWTPM_NO_NV
test_fwtpm_da_persist_failedtries();
test_fwtpm_da_nonorderly_penalty();
test_fwtpm_da_reboot_in_lockout_recovers();
test_fwtpm_da_lockoutauth_persist();
#endif
#endif
#endif
/* Policy */

View File

@ -38,5 +38,6 @@ endif
# TPM make check: manages server, runs unit.test + run_examples.sh
# Used for --enable-fwtpm builds
if BUILD_FWTPM
dist_noinst_SCRIPTS += tests/fwtpm_check.sh
dist_noinst_SCRIPTS += tests/fwtpm_check.sh \
tests/fwtpm_da_retry.sh
endif

View File

@ -225,6 +225,22 @@ static void test_wolfTPM2_GetCapabilities(void)
rc == 0 ? "Passed" : "Failed");
}
static void test_wolfTPM2_DictionaryAttack(void)
{
/* Argument validation (non-destructive; the functional lockout/recovery
* path is exercised by examples/management/da_check and the fwTPM unit
* tests). */
AssertIntEQ(wolfTPM2_DictionaryAttackLockReset(NULL), BAD_FUNC_ARG);
AssertIntEQ(wolfTPM2_DictionaryAttackParameters(NULL, 32, 0, 0),
BAD_FUNC_ARG);
/* newMaxTries of 0 is rejected client-side */
AssertIntEQ(wolfTPM2_DictionaryAttackParameters((WOLFTPM2_DEV*)1, 0, 0, 0),
BAD_FUNC_ARG);
printf("Test TPM Wrapper: %-40s %s\n", "Dictionary Attack args:",
"Passed");
}
/* test for wolfTPM2_ReadPublicKey */
static void test_wolfTPM2_ReadPublicKey(void)
{
@ -5635,6 +5651,7 @@ int unit_tests(int argc, char *argv[])
test_wolfTPM2_OpenExisting();
test_wolfTPM2_GetCapabilities();
test_wolfTPM2_GetRandom();
test_wolfTPM2_DictionaryAttack();
test_wolfTPM2_HashFinish_BufferTooSmall();
test_TPM2_PCRSel();
test_TPM2_Policy_NULL_Args();

View File

@ -110,6 +110,27 @@
#define FWTPM_MAX_RANDOM_BYTES 48
#endif
/* Dictionary Attack (DA) feature toggles:
* FWTPM_NO_DA - compile out all DA lockout protection.
* FWTPM_DA_USED_RETRY - emulate real-TPM behavior where the first use of a
* DA-protected (non-noDA) authorization after startup
* persists the daUsed flag to NV and returns
* TPM_RC_RETRY, prompting the caller to resubmit.
* Off by default. The client does not auto-resubmit;
* callers must resend the command on TPM_RC_RETRY. */
#ifndef FWTPM_DA_DEFAULT_MAX_TRIES
#define FWTPM_DA_DEFAULT_MAX_TRIES 32
#endif
#ifndef FWTPM_DA_DEFAULT_RECOVERY
#define FWTPM_DA_DEFAULT_RECOVERY 600 /* seconds */
#endif
#ifndef FWTPM_DA_DEFAULT_LOCKOUT_RECOVERY
#define FWTPM_DA_DEFAULT_LOCKOUT_RECOVERY 86400 /* seconds */
#endif
#ifndef FWTPM_DA_MAX_TRIES_LIMIT
#define FWTPM_DA_MAX_TRIES_LIMIT 0xFFFF
#endif
/* Maximum transient objects loaded at once (TPM 2.0 spec minimum: 3) */
#ifndef FWTPM_MAX_OBJECTS
#define FWTPM_MAX_OBJECTS 3
@ -633,10 +654,16 @@ typedef struct FWTPM_CTX {
int globalNvWriteLock; /* NV_GlobalWriteLock (reset on Startup CLEAR) */
#ifndef FWTPM_NO_DA
/* Dictionary Attack protection state */
UINT32 daFailedTries; /* Current failed auth count (volatile) */
UINT32 daFailedTries; /* Failed auth count, persisted in NV */
UINT32 daMaxTries; /* Threshold before lockout (default 32) */
UINT32 daRecoveryTime; /* Seconds to decrement failedTries */
UINT32 daLockoutRecovery; /* Seconds to fully recover. 0=reboot only */
UINT64 daSelfHealMs; /* Clock ms baseline for failedTries self-heal */
UINT64 daLockoutHealMs; /* Clock ms when lockoutAuthFailed was set */
int daUsed; /* DA-protected auth used this boot (volatile) */
int orderly; /* 1 = last NV checkpoint was clean (persisted) */
int lockoutAuthFailed; /* lockoutAuth lock; persisted (reboot-clears
* only when clockless or lockoutRecovery==0) */
#endif
int activeLocality;
UINT64 clockOffset; /* Clock offset set by ClockSet */

View File

@ -3232,6 +3232,38 @@ WOLFTPM_API int wolfTPM2_UnloadHandle(WOLFTPM2_DEV* dev, WOLFTPM2_HANDLE* handle
*/
WOLFTPM_API int wolfTPM2_Clear(WOLFTPM2_DEV* dev);
/*!
\ingroup wolfTPM2_Wrappers
\brief Reset the dictionary attack lockout failed-tries counter
\return TPM_RC_SUCCESS: successful
\return TPM_RC_LOCKOUT: lockoutAuth is in its recovery interval
\return BAD_FUNC_ARG: check the provided arguments
\param dev pointer to a TPM2_DEV struct
\sa wolfTPM2_DictionaryAttackParameters
*/
WOLFTPM_API int wolfTPM2_DictionaryAttackLockReset(WOLFTPM2_DEV* dev);
/*!
\ingroup wolfTPM2_Wrappers
\brief Set the dictionary attack lockout parameters
\return TPM_RC_SUCCESS: successful
\return TPM_RC_LOCKOUT: lockoutAuth is in its recovery interval
\return BAD_FUNC_ARG: check the provided arguments
\param dev pointer to a TPM2_DEV struct
\param newMaxTries failed auths before lockout (must be non-zero)
\param newRecoveryTime seconds to forgive one failed try (0 = reboot only)
\param lockoutRecovery seconds before a failed lockoutAuth recovers
\sa wolfTPM2_DictionaryAttackLockReset
*/
WOLFTPM_API int wolfTPM2_DictionaryAttackParameters(WOLFTPM2_DEV* dev,
word32 newMaxTries, word32 newRecoveryTime, word32 lockoutRecovery);
/*!
\ingroup wolfTPM2_Wrappers
\brief Helper function to start a TPM generated hash