mirror of https://github.com/wolfSSL/wolfTPM.git
Merge pull request #541 from aidangarske/fwtpm-da-hardening
fwTPM: Dictionary Attack (DA) hardening and DA/noDA test coveragepull/542/head
commit
1601a78890
|
|
@ -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.
|
||||
|
|
|
|||
12
ChangeLog.md
12
ChangeLog.md
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue