fwTPM: append-only NV journal for write-once flash ports and swtpm UART symlink support

pull/540/head
David Garske 2026-06-24 14:57:53 -07:00
parent c5c971cb0e
commit b8709b13dd
11 changed files with 925 additions and 77 deletions

View File

@ -27,4 +27,4 @@ jobs:
uses: codespell-project/actions-codespell@v2
with:
skip: .git,./IDE,./certs,./m4,*.der,*.pem
ignore_words_list: inh,inout,keypair,nd,parm,rcv,ser,loadIn,importIn,certifyIn,bu,fo,daa,pris,hsi
ignore_words_list: inh,inout,keypair,nd,parm,rcv,ser,loadIn,importIn,certifyIn,bu,fo,daa,pris,hsi,fram,hart

View File

@ -39,6 +39,10 @@ set(WOLFTPM_FWTPM_FUZZ "no" CACHE STRING
"Enable fwTPM fuzz target (requires libFuzzer, default: disabled)")
set_property(CACHE WOLFTPM_FWTPM_FUZZ
PROPERTY STRINGS "yes;no")
set(WOLFTPM_FWTPM_NV_APPEND_ONLY "no" CACHE STRING
"Enable append-only NV journal for write-once flash fwTPM ports (default: disabled)")
set_property(CACHE WOLFTPM_FWTPM_NV_APPEND_ONLY
PROPERTY STRINGS "yes;no")
# fwtpm-only forces fwtpm on
if(WOLFTPM_FWTPM_ONLY)
@ -523,6 +527,9 @@ if(WOLFTPM_FWTPM)
if(WOLFTPM_FWTPM_TIS)
target_compile_definitions(fwtpm_server PRIVATE "WOLFTPM_FWTPM_TIS")
endif()
if(WOLFTPM_FWTPM_NV_APPEND_ONLY)
target_compile_definitions(fwtpm_server PRIVATE "WOLFTPM_FWTPM_NV_APPEND_ONLY")
endif()
target_link_libraries(fwtpm_server PRIVATE wolftpm_wolfssl_dep)
target_include_directories(fwtpm_server PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
@ -546,6 +553,9 @@ if(WOLFTPM_FWTPM)
"WOLFTPM_FWTPM"
"FWTPM_NV_FILE=\"fwtpm_test_nv.bin\""
)
if(WOLFTPM_FWTPM_NV_APPEND_ONLY)
target_compile_definitions(fwtpm_unit_test PRIVATE "WOLFTPM_FWTPM_NV_APPEND_ONLY")
endif()
target_link_libraries(fwtpm_unit_test PRIVATE wolftpm_wolfssl_dep)
target_include_directories(fwtpm_unit_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
@ -562,6 +572,9 @@ if(WOLFTPM_FWTPM)
${FWTPM_CORE_SOURCES}
)
target_compile_definitions(fwtpm_fuzz PRIVATE "WOLFTPM_FWTPM")
if(WOLFTPM_FWTPM_NV_APPEND_ONLY)
target_compile_definitions(fwtpm_fuzz PRIVATE "WOLFTPM_FWTPM_NV_APPEND_ONLY")
endif()
target_link_libraries(fwtpm_fuzz PRIVATE wolftpm_wolfssl_dep)
target_include_directories(fwtpm_fuzz PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}

View File

@ -415,6 +415,17 @@ then
fi
fi
# Append-only NV journal for write-once flash fwTPM ports (opt-in)
AC_ARG_ENABLE([fwtpm-nv-appendonly],
[AS_HELP_STRING([--enable-fwtpm-nv-appendonly],[Enable the append-only NV journal mode for write-once flash fwTPM ports (default: disabled)])],
[ ENABLED_FWTPM_NV_APPENDONLY=$enableval ],
[ ENABLED_FWTPM_NV_APPENDONLY=no ]
)
if test "x$ENABLED_FWTPM_NV_APPENDONLY" = "xyes"
then
AM_CFLAGS="$AM_CFLAGS -DWOLFTPM_FWTPM_NV_APPEND_ONLY"
fi
# Fuzz target
AC_ARG_ENABLE([fuzz],
[AS_HELP_STRING([--enable-fuzz],[Enable fuzz targets (default: disabled)])],

View File

@ -43,7 +43,7 @@ fwTPM can replace a hardware TPM for:
|------|------|
| `fwtpm_command.c` | TPM 2.0 command processor and dispatch table (~9500 lines) |
| `fwtpm_io.c` | Transport layer -- SWTPM TCP socket protocol (default) |
| `fwtpm_nv.c` | NV storage -- file-based (default), HAL-abstracted for embedded |
| `fwtpm_nv.c` | NV storage -- file-based (default); HAL-abstracted, with a built-in append-only mode for write-once flash |
| `fwtpm_tis.c` | TIS register state machine (transport-agnostic) |
| `fwtpm_tis_shm.c` | POSIX shared memory + semaphore TIS transport |
| `fwtpm_main.c` | Server entry point, CLI argument parsing |
@ -120,6 +120,7 @@ make
| `--enable-fwtpm` | Build `fwtpm_server` binary (alongside client library) |
| `--enable-fwtpm-only` | Build only `fwtpm_server` (no client library, examples, or tests) |
| `--enable-swtpm` | Use SWTPM TCP socket transport (ports 2321/2322) |
| `--enable-fwtpm-nv-appendonly` | Append-only NV journal for write-once flash ports (off by default) |
| `--enable-debug` | Enable debug logging |
| Compile Define | Set By |
@ -129,6 +130,7 @@ make
| `WOLFTPM_FWTPM_HAL` | `--enable-fwtpm` without `--enable-swtpm` |
| `WOLFTPM_FWTPM_TIS` | `--enable-fwtpm` without `--enable-swtpm` |
| `WOLFTPM_ADV_IO` | Set with `WOLFTPM_FWTPM_HAL` |
| `WOLFTPM_FWTPM_NV_APPEND_ONLY` | `--enable-fwtpm-nv-appendonly` (CMake `WOLFTPM_FWTPM_NV_APPEND_ONLY=yes`) |
## Usage
@ -190,6 +192,51 @@ NV indices) in `fwtpm_nv.bin` (configurable via `FWTPM_NV_FILE`). On first
start, seeds are randomly generated and saved. Subsequent starts reload
existing state.
#### NV Storage HAL (embedded ports)
NV access is abstracted behind `FWTPM_NV_HAL` (`read`/`write`/`erase`/`ctx`/
`maxSize`/`get_integrity_key`). The default backend is the file above; an
embedded port supplies its own HAL and registers it before `FWTPM_Init()`.
The journal is log-structured. On a byte-addressable backend (the file
default) it writes TLV entries at byte-granular offsets, rewrites the header in
place, and rewrites a trailing integrity MAC after every append. Internal flash
and NOR are write-once and program-granularity aligned, so they cannot service
in-place rewrites. Build with `--enable-fwtpm-nv-appendonly`
(`-DWOLFTPM_FWTPM_NV_APPEND_ONLY`) and set `appendOnly` and `writeAlign` on the
HAL; the journal then runs in append-only mode: the header is written only at
compaction, `writePos` is derived by scanning on load, and each commit is
sealed with an appended MAC-checkpoint entry padded up to `writeAlign`. The
existing `read`/`write`/`erase` HAL is the integration point - no separate
adapter.
```c
/* Native flash HAL. In append-only mode the journal only ever calls write()
* with writeAlign-aligned, forward, into-erased bytes, so write() is a simple
* flash program; erase() erases the region (sector loop); read() reads raw. */
FWTPM_NV_HAL hal;
XMEMSET(&hal, 0, sizeof(hal));
hal.read = myRead; hal.write = myProgram; hal.erase = myErase;
hal.ctx = myCtx; hal.maxSize = NV_SIZE;
hal.appendOnly = 1;
hal.writeAlign = PROG_SIZE; /* flash word size, e.g. 16 (STM32H5) */
hal.get_integrity_key = myDeviceSecret;/* recommended on flash */
FWTPM_NV_SetHAL(&ctx, &hal); /* before FWTPM_Init() */
```
In append-only mode the journal buffers a pending program granule internally and
flushes full, aligned granules through `write()`, so a programmed cell is never
rewritten and a whole sector is erased only on compaction. The header sector is
therefore not erased on every append, and a torn final commit (e.g. on power
loss) is ignored on the next load while all previously committed state survives.
A `get_integrity_key` callback is strongly recommended on flash so the MAC
checkpoints authenticate the journal and reject a torn or tampered tail.
`writeAlign <= 1` selects no buffering, so byte-writable NV (EEPROM/FRAM) works
with a plain `write()`. Compaction still erases the whole region before
rewriting, so a power loss during compaction itself remains a vulnerable window
(a future two-region ping-pong layout would close it). See
`src/fwtpm/ports/README.md` for the full porting guide.
## Supported TPM 2.0 Commands
@ -520,6 +567,7 @@ All macros are compile-time overridable (e.g., `-DFWTPM_MAX_OBJECTS=8`).
| `FWTPM_CMD_PORT` | 2321 | Default TCP command port |
| `FWTPM_PLAT_PORT` | 2322 | Default TCP platform port |
| `FWTPM_NV_FILE` | `"fwtpm_nv.bin"` | Default NV storage file path |
| `FWTPM_NV_MAX_WRITE_ALIGN` | 64 | Max append-only program granule in bytes (upper bound on a HAL's `writeAlign`); sizes the pending-granule buffer when `WOLFTPM_FWTPM_NV_APPEND_ONLY` is set |
| `FWTPM_PCR_BANKS` | 2 | Number of PCR banks (SHA-256 + SHA-384) |
| `FWTPM_TIS_BURST_COUNT` | 64 | TIS FIFO burst count (bytes per transfer) |
| `FWTPM_TIS_FIFO_SIZE` | 4096 | TIS command/response FIFO size |

View File

@ -59,6 +59,16 @@
/* TLV header size: tag(2) + length(2) */
#define TLV_HDR_SIZE 4
/* Append-only is active only when both the HAL opts in and the feature is
* built. Reading the field through this macro keeps the byte-addressable MAC
* write/verify and writePos validation active if a port sets appendOnly
* without WOLFTPM_FWTPM_NV_APPEND_ONLY (rather than silently dropping them). */
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
#define FW_NV_APPEND_ONLY(hal) ((hal)->appendOnly)
#else
#define FW_NV_APPEND_ONLY(hal) (0)
#endif
/* ========================================================================= */
/* File-based NV backend */
/* ========================================================================= */
@ -171,7 +181,9 @@ static FWTPM_NV_HAL fwNvDefaultHal = {
FwNvFileErase,
(void*)FWTPM_NV_FILE,
FWTPM_NV_MAX_SIZE,
NULL /* get_integrity_key: file backend uses a key file */
NULL, /* get_integrity_key: file backend uses a key file */
0, /* writeAlign: none */
0 /* appendOnly: 0 (byte-addressable) */
};
#endif /* !NO_FILESYSTEM */
@ -788,11 +800,96 @@ static int FwNvGetIntegrityKey(FWTPM_CTX* ctx, byte* key, word32* keySz)
return 0;
}
/* Write via the HAL. Byte-addressable backends pass through. In append-only
* mode (writeAlign > 1) writes are forward into erased space; bytes accumulate
* in a pending program granule, flushed once full so a cell is never rewritten
* and hal->write is a plain program. A new granule must start aligned. */
static int FwNvHalWrite(FWTPM_CTX* ctx, word32 offset, const byte* buf,
word32 size)
{
FWTPM_NV_HAL* hal = &ctx->nvHal;
if (hal->write == NULL) {
return TPM_RC_FAILURE;
}
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
if (FW_NV_APPEND_ONLY(hal) && hal->writeAlign > 1) {
word32 align = hal->writeAlign;
word32 src = 0;
word32 n;
int rc;
if (size == 0) {
return TPM_RC_SUCCESS;
}
if (size > hal->maxSize || offset > hal->maxSize - size) {
return TPM_RC_FAILURE;
}
if (ctx->nvGranuleFill == 0 ||
offset != ctx->nvGranuleBase + ctx->nvGranuleFill) {
if (ctx->nvGranuleFill != 0 || (offset % align) != 0) {
return TPM_RC_FAILURE; /* must start a granule aligned */
}
ctx->nvGranuleBase = offset;
}
while (src < size) {
n = align - ctx->nvGranuleFill;
if (n > size - src) {
n = size - src;
}
XMEMCPY((byte*)ctx->nvGranule + ctx->nvGranuleFill, buf + src, n);
ctx->nvGranuleFill += n;
src += n;
if (ctx->nvGranuleFill == align) {
rc = hal->write(hal->ctx, ctx->nvGranuleBase,
(const byte*)ctx->nvGranule, align);
if (rc != 0) {
return TPM_RC_FAILURE;
}
ctx->nvGranuleBase += align;
ctx->nvGranuleFill = 0;
}
}
return TPM_RC_SUCCESS;
}
#endif
return hal->write(hal->ctx, offset, buf, size);
}
/* Read via the HAL, overlaying the pending program granule (append-only) so a
* freshly appended entry reads back before it is flushed (e.g. for the MAC). */
static int FwNvHalRead(FWTPM_CTX* ctx, word32 offset, byte* buf, word32 size)
{
FWTPM_NV_HAL* hal = &ctx->nvHal;
int rc;
if (hal->read == NULL) {
return TPM_RC_FAILURE;
}
rc = hal->read(hal->ctx, offset, buf, size);
if (rc != 0) {
return TPM_RC_FAILURE;
}
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
if (FW_NV_APPEND_ONLY(hal) && hal->writeAlign > 1 &&
ctx->nvGranuleFill > 0 && size > 0) {
word32 pStart = ctx->nvGranuleBase;
word32 pEnd = ctx->nvGranuleBase + ctx->nvGranuleFill;
word32 a = (offset > pStart) ? offset : pStart;
word32 b = ((offset + size) < pEnd) ? (offset + size) : pEnd;
if (a < b) {
XMEMCPY(buf + (a - offset),
(const byte*)ctx->nvGranule + (a - pStart), b - a);
}
}
#endif
return TPM_RC_SUCCESS;
}
/* HMAC-SHA256 over the journal body [header .. writePos). */
static int FwNvComputeJournalMac(FWTPM_CTX* ctx, const byte* key,
word32 keySz, byte* macOut)
{
FWTPM_NV_HAL* hal = &ctx->nvHal;
FWTPM_DECLARE_VAR(hmac, Hmac);
byte chunk[256];
word32 off = sizeof(FWTPM_NV_HEADER);
@ -809,7 +906,7 @@ static int FwNvComputeJournalMac(FWTPM_CTX* ctx, const byte* key,
if (n > sizeof(chunk)) {
n = sizeof(chunk);
}
rc = hal->read(hal->ctx, off, chunk, n);
rc = FwNvHalRead(ctx, off, chunk, n);
if (rc == 0) {
rc = wc_HmacUpdate(hmac, chunk, n);
}
@ -843,27 +940,101 @@ static int FwNvWriteHeader(FWTPM_CTX* ctx)
FwStoreU32LE(hdr + 8, ctx->nvWritePos);
FwStoreU32LE(hdr + 12, hal->maxSize);
rc = hal->write(hal->ctx, 0, hdr, sizeof(hdr));
rc = FwNvHalWrite(ctx, 0, hdr, sizeof(hdr));
/* Refresh the trailing journal MAC so a tampered journal is detected
* on the next load. The MAC sits at writePos and is rewritten after
* every append. */
if (rc == TPM_RC_SUCCESS && FwNvGetIntegrityKey(ctx, key, &keySz)) {
/* Rewrite the trailing MAC in place (byte-addressable only; append-only
* commits integrity via FwNvAppendCheckpoint instead). */
if (rc == TPM_RC_SUCCESS && !FW_NV_APPEND_ONLY(hal) &&
FwNvGetIntegrityKey(ctx, key, &keySz)) {
rc = FwNvComputeJournalMac(ctx, key, keySz, mac);
if (rc == TPM_RC_SUCCESS) {
rc = hal->write(hal->ctx, ctx->nvWritePos, mac, sizeof(mac));
rc = FwNvHalWrite(ctx, ctx->nvWritePos, mac, sizeof(mac));
}
TPM2_ForceZero(key, sizeof(key));
}
return rc;
}
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
/* Append-only commit: append a MAC-checkpoint entry over the body so far, with
* 0xFF pad in its value to round writePos up to writeAlign (the next commit
* then starts on a fresh program granule; the loader skips it on replay). */
static int FwNvAppendCheckpoint(FWTPM_CTX* ctx)
{
FWTPM_NV_HAL* hal = &ctx->nvHal;
byte key[FWTPM_NV_KEY_SIZE];
byte mac[FWTPM_NV_MAC_SIZE];
byte tlvHdr[TLV_HDR_SIZE];
byte ff[64];
word32 keySz = 0;
word32 macLen = 0;
word32 align;
word32 padLen;
word32 valueLen;
word32 padDone;
word32 padChunk;
int haveKey;
int rc = TPM_RC_SUCCESS;
if (hal->write == NULL) {
return TPM_RC_FAILURE;
}
/* MAC over [header .. current writePos) (this entry not included). */
haveKey = FwNvGetIntegrityKey(ctx, key, &keySz);
if (haveKey) {
rc = FwNvComputeJournalMac(ctx, key, keySz, mac);
TPM2_ForceZero(key, sizeof(key));
if (rc != TPM_RC_SUCCESS) {
return rc;
}
macLen = FWTPM_NV_MAC_SIZE;
}
/* Pad so writePos lands on a writeAlign boundary after this entry. */
align = hal->writeAlign;
if (align < 1) {
align = 1;
}
padLen = (align - ((ctx->nvWritePos + TLV_HDR_SIZE + macLen) % align))
% align;
valueLen = macLen + padLen;
if (valueLen > 0xFFFF) {
return TPM_RC_FAILURE; /* writeAlign too large for a TLV length */
}
FwStoreU16LE(tlvHdr, FWTPM_NV_TAG_MAC);
FwStoreU16LE(tlvHdr + 2, (UINT16)valueLen);
rc = FwNvHalWrite(ctx, ctx->nvWritePos, tlvHdr, TLV_HDR_SIZE);
if (rc == TPM_RC_SUCCESS && macLen > 0) {
rc = FwNvHalWrite(ctx, ctx->nvWritePos + TLV_HDR_SIZE, mac, macLen);
}
/* Write 0xFF alignment pad (0xFF leaves write-once cells erased). */
XMEMSET(ff, 0xFF, sizeof(ff));
padDone = 0;
while (rc == TPM_RC_SUCCESS && padDone < padLen) {
padChunk = padLen - padDone;
if (padChunk > sizeof(ff)) {
padChunk = sizeof(ff);
}
rc = FwNvHalWrite(ctx,
ctx->nvWritePos + TLV_HDR_SIZE + macLen + padDone, ff, padChunk);
padDone += padChunk;
}
if (rc == TPM_RC_SUCCESS) {
ctx->nvWritePos += TLV_HDR_SIZE + valueLen;
}
return rc;
}
#endif /* WOLFTPM_FWTPM_NV_APPEND_ONLY */
/* Append a single TLV entry to the journal */
static int FwNvAppendEntry(FWTPM_CTX* ctx, UINT16 tag,
const byte* value, UINT16 valueLen)
{
FWTPM_NV_HAL* hal = &ctx->nvHal;
word32 entrySize = TLV_HDR_SIZE + valueLen;
word32 reserve = FWTPM_NV_MAC_SIZE;
byte tlvHdr[TLV_HDR_SIZE];
int rc;
@ -871,8 +1042,13 @@ static int FwNvAppendEntry(FWTPM_CTX* ctx, UINT16 tag,
return TPM_RC_FAILURE;
}
/* Append-only also appends a checkpoint and rounds up to a granule. */
if (FW_NV_APPEND_ONLY(hal)) {
reserve = TLV_HDR_SIZE + FWTPM_NV_MAC_SIZE + hal->writeAlign;
}
/* Reserve room for the trailing journal MAC written after each append */
if (ctx->nvWritePos + entrySize + FWTPM_NV_MAC_SIZE > hal->maxSize) {
if (ctx->nvWritePos + entrySize + reserve > hal->maxSize) {
/* If already compacting, NV is genuinely full */
if (ctx->nvCompacting) {
return TPM_RC_NV_SPACE;
@ -883,7 +1059,7 @@ static int FwNvAppendEntry(FWTPM_CTX* ctx, UINT16 tag,
return rc;
}
/* After compaction, check again */
if (ctx->nvWritePos + entrySize + FWTPM_NV_MAC_SIZE > hal->maxSize) {
if (ctx->nvWritePos + entrySize + reserve > hal->maxSize) {
return TPM_RC_NV_SPACE;
}
}
@ -892,15 +1068,25 @@ static int FwNvAppendEntry(FWTPM_CTX* ctx, UINT16 tag,
FwStoreU16LE(tlvHdr, tag);
FwStoreU16LE(tlvHdr + 2, valueLen);
rc = hal->write(hal->ctx, ctx->nvWritePos, tlvHdr, TLV_HDR_SIZE);
rc = FwNvHalWrite(ctx, ctx->nvWritePos, tlvHdr, TLV_HDR_SIZE);
if (rc == TPM_RC_SUCCESS && valueLen > 0) {
rc = hal->write(hal->ctx, ctx->nvWritePos + TLV_HDR_SIZE,
rc = FwNvHalWrite(ctx, ctx->nvWritePos + TLV_HDR_SIZE,
value, valueLen);
}
if (rc == TPM_RC_SUCCESS) {
ctx->nvWritePos += entrySize;
/* Update header with new writePos */
rc = FwNvWriteHeader(ctx);
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
/* Commit via a checkpoint; compaction emits one at the end instead. */
if (FW_NV_APPEND_ONLY(hal)) {
if (!ctx->nvCompacting) {
rc = FwNvAppendCheckpoint(ctx);
}
}
else
#endif
{
rc = FwNvWriteHeader(ctx); /* byte-addressable: header + MAC */
}
}
return rc;
}
@ -1241,6 +1427,239 @@ int FWTPM_NV_SetHAL(FWTPM_CTX* ctx, FWTPM_NV_HAL* hal)
return TPM_RC_SUCCESS;
}
/* Generate fresh hierarchy seeds and default state, then persist via a compact
* save. Used on first run and whenever an existing journal cannot be loaded or
* authenticated. */
static int FwNvGenFreshState(FWTPM_CTX* ctx)
{
int rc;
#ifdef DEBUG_WOLFTPM
printf("fwTPM: No NV state found, generating fresh seeds\n");
#endif
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->ownerSeed, FWTPM_SEED_SIZE);
if (rc == 0) {
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->endorsementSeed,
FWTPM_SEED_SIZE);
}
if (rc == 0) {
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->platformSeed,
FWTPM_SEED_SIZE);
}
if (rc == 0) {
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->nullSeed, FWTPM_SEED_SIZE);
}
if (rc != 0) {
return TPM_RC_FAILURE;
}
/* Auth values start empty */
XMEMSET(&ctx->ownerAuth, 0, sizeof(ctx->ownerAuth));
XMEMSET(&ctx->endorsementAuth, 0, sizeof(ctx->endorsementAuth));
XMEMSET(&ctx->platformAuth, 0, sizeof(ctx->platformAuth));
XMEMSET(&ctx->lockoutAuth, 0, sizeof(ctx->lockoutAuth));
#ifndef FWTPM_NO_DA
/* DA protection defaults */
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 */
ctx->pcrAllocatedBanks = FWTPM_PCR_ALLOC_DEFAULT;
/* Save initial state (compact write) */
rc = FWTPM_NV_Save(ctx);
#ifdef DEBUG_WOLFTPM
if (rc != TPM_RC_SUCCESS) {
printf("fwTPM: Warning: Failed to save initial NV state (%d)\n", rc);
}
#endif
return rc;
}
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
/* Decide whether a checkpoint entry that failed authentication is a torn
* (power-loss) write rather than tamper. Append-only programs whole writeAlign
* granules in order, so a torn checkpoint leaves its trailing granule erased
* (all 0xFF); a completely written checkpoint always has real bytes there. A
* storage attacker can only program bits (1->0), so an erased trailing granule
* cannot be forged. Returns 1 if the entry looks torn, 0 if fully written. */
static int FwNvCheckpointTorn(FWTPM_CTX* ctx, word32 entryPos, word32 entryLen)
{
FWTPM_NV_HAL* hal = &ctx->nvHal;
byte tail[FWTPM_NV_MAX_WRITE_ALIGN];
word32 align = hal->writeAlign;
word32 i;
if (align <= 1 || align > sizeof(tail) || entryLen < align) {
return 0; /* no granule semantics: treat a failing checkpoint as tamper */
}
if (hal->read(hal->ctx, entryPos + entryLen - align, tail, align)
!= TPM_RC_SUCCESS) {
return 0;
}
for (i = 0; i < align; i++) {
if (tail[i] != 0xFF) {
return 0; /* trailing granule programmed: fully written */
}
}
return 1; /* trailing granule erased: torn write */
}
/* Load an append-only journal: scan for the last MAC checkpoint that
* authenticates the body before it, replay only the entries it covers, and
* rewrite cleanly if a torn trailing commit remains. A fully written
* checkpoint that fails authentication after a valid one is treated as
* tamper/rollback and rejected (only a torn trailing granule is recovered).
* Returns TPM_RC_SUCCESS (loaded), TPM_RC_NV_UNINITIALIZED (regenerate), or a
* negative error. */
static int FwNvInitAppendOnly(FWTPM_CTX* ctx, byte** valueBufP,
word32* valueBufSzP)
{
FWTPM_NV_HAL* hal = &ctx->nvHal;
byte tlvHdr[TLV_HDR_SIZE];
byte vKey[FWTPM_NV_KEY_SIZE];
byte storedMac[FWTPM_NV_MAC_SIZE];
byte calcMac[FWTPM_NV_MAC_SIZE];
byte* valueBuf = *valueBufP;
word32 valueBufSz = *valueBufSzP;
word32 vKeySz = 0;
word32 scanPos;
word32 lastMacStart = 0;
word32 lastMacEnd = 0;
word32 committedEnd;
word32 pos;
UINT16 tag, len;
int haveKey;
int tamper = 0;
int dirtyTail = 0;
int rc = TPM_RC_SUCCESS;
haveKey = FwNvGetIntegrityKey(ctx, vKey, &vKeySz);
/* Pass 1: locate the last authenticated MAC checkpoint. */
scanPos = (word32)sizeof(FWTPM_NV_HEADER);
while (scanPos + TLV_HDR_SIZE <= hal->maxSize) {
if (hal->read(hal->ctx, scanPos, tlvHdr, TLV_HDR_SIZE)
!= TPM_RC_SUCCESS) {
break;
}
tag = FwLoadU16LE(tlvHdr);
len = FwLoadU16LE(tlvHdr + 2);
if (tag == FWTPM_NV_TAG_FREE) {
break; /* erased space: end of journal */
}
if (scanPos + TLV_HDR_SIZE + len > hal->maxSize) {
/* Non-free tag with no room for its body: a torn partial header
* (e.g. tag programmed, length still erased) left at the append
* offset. Mark the tail dirty so it is compacted away. */
dirtyTail = 1;
break;
}
if (tag == FWTPM_NV_TAG_MAC) {
int ok = 1;
if (haveKey) {
ok = 0;
if (len >= FWTPM_NV_MAC_SIZE) {
/* MAC covers [header, scanPos). */
ctx->nvWritePos = scanPos;
if (hal->read(hal->ctx, scanPos + TLV_HDR_SIZE, storedMac,
FWTPM_NV_MAC_SIZE) == TPM_RC_SUCCESS &&
FwNvComputeJournalMac(ctx, vKey, vKeySz, calcMac)
== TPM_RC_SUCCESS &&
TPM2_ConstantCompare(storedMac, calcMac,
FWTPM_NV_MAC_SIZE) == 0) {
ok = 1;
}
}
}
if (ok) {
lastMacStart = scanPos;
lastMacEnd = scanPos + TLV_HDR_SIZE + len;
}
else if (haveKey && len >= FWTPM_NV_MAC_SIZE && lastMacEnd != 0 &&
!FwNvCheckpointTorn(ctx, scanPos, TLV_HDR_SIZE + len)) {
/* A fully written checkpoint that fails authentication after a
* valid one is tamper/rollback, not a torn tail: reject. */
tamper = 1;
break;
}
}
scanPos += TLV_HDR_SIZE + len;
}
if (haveKey) {
TPM2_ForceZero(vKey, sizeof(vKey));
}
if (tamper || lastMacEnd == 0) {
/* Detected tamper, or no authenticated checkpoint at all. A valid save
* always ends with a checkpoint (keyed or not), so an uncheckpointed
* image is incomplete: discard (caller regenerates fresh state). */
return TPM_RC_NV_UNINITIALIZED;
}
committedEnd = lastMacStart; /* replay data before this checkpoint */
ctx->nvWritePos = lastMacEnd; /* next append position (aligned) */
/* Pass 2: replay committed data entries (skip MAC checkpoints). */
pos = (word32)sizeof(FWTPM_NV_HEADER);
while (pos + TLV_HDR_SIZE <= committedEnd) {
if (hal->read(hal->ctx, pos, tlvHdr, TLV_HDR_SIZE) != TPM_RC_SUCCESS) {
rc = TPM_RC_FAILURE;
break;
}
tag = FwLoadU16LE(tlvHdr);
len = FwLoadU16LE(tlvHdr + 2);
if (tag == FWTPM_NV_TAG_FREE) {
break;
}
if (pos + TLV_HDR_SIZE + len > committedEnd) {
break;
}
if (tag != FWTPM_NV_TAG_MAC) {
if (len > valueBufSz) {
byte* newBuf;
newBuf = (byte*)XMALLOC(len, NULL, DYNAMIC_TYPE_TMP_BUFFER);
if (newBuf == NULL) {
rc = TPM_RC_MEMORY;
break;
}
XFREE(valueBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER);
valueBuf = newBuf;
valueBufSz = len;
}
if (len > 0) {
if (hal->read(hal->ctx, pos + TLV_HDR_SIZE, valueBuf, len)
!= TPM_RC_SUCCESS) {
rc = TPM_RC_FAILURE;
break;
}
}
FwNvProcessEntry(ctx, tag, valueBuf, len);
}
pos += TLV_HDR_SIZE + len;
}
*valueBufP = valueBuf;
*valueBufSzP = valueBufSz;
/* A torn commit after the last good checkpoint (trailing data entries, or
* a partial header at the append offset) leaves un-erased cells that block
* future appends; rewrite the recovered state cleanly. */
if (rc == TPM_RC_SUCCESS && (scanPos > lastMacEnd || dirtyTail)) {
rc = FWTPM_NV_Save(ctx);
}
return rc;
}
#endif /* WOLFTPM_FWTPM_NV_APPEND_ONLY */
int FWTPM_NV_Init(FWTPM_CTX* ctx)
{
int rc;
@ -1273,6 +1692,13 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx)
}
#endif
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
/* Append-only program granule must fit the in-context staging buffer. */
if (FW_NV_APPEND_ONLY(hal) && hal->writeAlign > FWTPM_NV_MAX_WRITE_ALIGN) {
return BAD_FUNC_ARG;
}
#endif
/* Allocate value read buffer */
valueBuf = (byte*)XMALLOC(valueBufSz, NULL, DYNAMIC_TYPE_TMP_BUFFER);
if (valueBuf == NULL) {
@ -1289,7 +1715,7 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx)
hdr.maxSize = FwLoadU32LE(hdrBuf + 12);
}
if (rc == TPM_RC_SUCCESS &&
if (rc == TPM_RC_SUCCESS && !FW_NV_APPEND_ONLY(hal) &&
hdr.magic == FWTPM_NV_MAGIC &&
hdr.version == FWTPM_NV_VERSION) {
/* Validate writePos against HAL bounds before using it */
@ -1300,10 +1726,10 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx)
}
}
/* Authenticate the journal before replaying it. A failed MAC means the
* journal was tampered, so it is discarded and fresh state generated
* rather than loading forged objects, clock, PCR state, or keys. */
if (rc == TPM_RC_SUCCESS &&
/* Authenticate the journal before replaying it; a failed MAC discards it
* and regenerates rather than loading forged state. (Append-only
* authenticates per-checkpoint in FwNvInitAppendOnly instead.) */
if (rc == TPM_RC_SUCCESS && !FW_NV_APPEND_ONLY(hal) &&
hdr.magic == FWTPM_NV_MAGIC &&
hdr.version == FWTPM_NV_VERSION) {
ctx->nvWritePos = hdr.writePos;
@ -1323,6 +1749,18 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx)
}
}
/* Append-only journals derive writePos by scanning (see FwNvInitAppendOnly). */
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
if (rc == TPM_RC_SUCCESS && FW_NV_APPEND_ONLY(hal) &&
hdr.magic == FWTPM_NV_MAGIC &&
hdr.version == FWTPM_NV_VERSION) {
rc = FwNvInitAppendOnly(ctx, &valueBuf, &valueBufSz);
if (rc == TPM_RC_NV_UNINITIALIZED) {
rc = FwNvGenFreshState(ctx); /* no authenticated journal */
}
}
else
#endif
if (rc == TPM_RC_SUCCESS &&
hdr.magic == FWTPM_NV_MAGIC &&
hdr.version == FWTPM_NV_VERSION) {
@ -1396,56 +1834,7 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx)
"(found %d, expected %d) — regenerating seeds\n",
(int)hdr.version, (int)FWTPM_NV_VERSION);
}
#ifdef DEBUG_WOLFTPM
printf("fwTPM: No NV state found, generating fresh seeds\n");
#endif
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->ownerSeed,
FWTPM_SEED_SIZE);
if (rc == 0) {
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->endorsementSeed,
FWTPM_SEED_SIZE);
}
if (rc == 0) {
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->platformSeed,
FWTPM_SEED_SIZE);
}
if (rc == 0) {
rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->nullSeed,
FWTPM_SEED_SIZE);
}
if (rc != 0) {
XFREE(valueBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER);
return TPM_RC_FAILURE;
}
/* Auth values start empty */
XMEMSET(&ctx->ownerAuth, 0, sizeof(ctx->ownerAuth));
XMEMSET(&ctx->endorsementAuth, 0, sizeof(ctx->endorsementAuth));
XMEMSET(&ctx->platformAuth, 0, sizeof(ctx->platformAuth));
XMEMSET(&ctx->lockoutAuth, 0, sizeof(ctx->lockoutAuth));
#ifndef FWTPM_NO_DA
/* DA protection defaults */
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 */
ctx->pcrAllocatedBanks = FWTPM_PCR_ALLOC_DEFAULT;
/* Save initial state (compact write) */
rc = FWTPM_NV_Save(ctx);
#ifdef DEBUG_WOLFTPM
if (rc != TPM_RC_SUCCESS) {
printf("fwTPM: Warning: Failed to save initial NV state (%d)\n",
rc);
}
#endif
rc = FwNvGenFreshState(ctx);
}
XFREE(valueBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER);
@ -1489,6 +1878,12 @@ int FWTPM_NV_Save(FWTPM_CTX* ctx)
rc = hal->erase(hal->ctx, 0, hal->maxSize);
}
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
/* Freshly erased: reset the pending granule so the rewrite streams from 0. */
ctx->nvGranuleBase = 0;
ctx->nvGranuleFill = 0;
#endif
/* Reset write position to after header (only if erase succeeded) */
if (rc == 0) {
ctx->nvWritePos = (word32)sizeof(FWTPM_NV_HEADER);
@ -1746,9 +2141,18 @@ int FWTPM_NV_Save(FWTPM_CTX* ctx)
}
}
/* Update header with final writePos */
/* Seal the journal: append-only adds one trailing checkpoint (header was
* written after the erase); byte-addressable updates the header in place. */
if (rc == 0) {
rc = FwNvWriteHeader(ctx);
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
if (FW_NV_APPEND_ONLY(hal)) {
rc = FwNvAppendCheckpoint(ctx);
}
else
#endif
{
rc = FwNvWriteHeader(ctx);
}
}
#ifdef DEBUG_WOLFTPM

View File

@ -66,3 +66,6 @@ tests_fuzz_fwtpm_fuzz_LDADD = $(LIB_STATIC_ADD)
endif
endif
# Ship the ports guide even when not built
EXTRA_DIST += src/fwtpm/ports/README.md

View File

@ -1,12 +1,16 @@
# fwTPM Embedded Ports
Platform-specific ports of the wolfTPM fwTPM server for embedded targets.
Full reference ports live in the
[wolftpm-examples](https://github.com/wolfSSL/wolftpm-examples) repository.
## Available Ports
| Port | Repository | Description |
|------|-----------|-------------|
| STM32 | [wolftpm-examples/STM32/fwtpm-stm32h5](https://github.com/wolfSSL/wolftpm-examples/pull/1) | STM32H5 Cortex-M33 with TrustZone (CMSE) |
| STM32H5 | [STM32/fwtpm-stm32h5](https://github.com/wolfSSL/wolftpm-examples/tree/main/STM32/fwtpm-stm32h5) | STM32H5 Cortex-M33 with TrustZone (CMSE); internal-flash NV |
| PolarFire SoC | [Microchip/fwtpm-polarfire-miv](https://github.com/wolfSSL/wolftpm-examples/tree/main/Microchip/fwtpm-polarfire-miv) | MPFS250T; fwTPM bare-metal in M-mode on a U54 RISC-V hart (HSS AMP alongside Linux), TIS over shared L2-LIM memory |
| Zynq UltraScale+ ZCU102 | [Xilinx/fwtpm-zcu102-r5](https://github.com/wolfSSL/wolftpm-examples/tree/main/Xilinx/fwtpm-zcu102-r5) | ZynqMP MPSoC; fwTPM bare-metal on the Cortex-R5 RPU pair in lock-step, OpenAMP RPMsg client on the A53 (PetaLinux); volatile DDR NV or persistent QSPI |
## Porting Guide
@ -15,6 +19,26 @@ To add a new platform, implement these HAL callbacks:
1. **NV Storage HAL** (`FWTPM_NV_HAL`): `read()`, `write()`, `erase()` for
persistent flash storage. Register via `FWTPM_NV_SetHAL()` before `FWTPM_Init()`.
The NV journal is log-structured. On a byte-addressable backend it writes at
byte-granular offsets and rewrites the header and a trailing integrity MAC in
place after every append. Internal flash and NOR are write-once and
program-granularity aligned, so they cannot service in-place rewrites. For
those, build with `--enable-fwtpm-nv-appendonly` (`-DWOLFTPM_FWTPM_NV_APPEND_ONLY`,
CMake `WOLFTPM_FWTPM_NV_APPEND_ONLY=yes`) and set `hal.appendOnly = 1` and
`hal.writeAlign = <program size>` (e.g. 16 on STM32H5) before
`FWTPM_NV_SetHAL()`. The journal then writes the
header only at compaction, derives writePos by scanning on load, seals each
commit with an appended, program-granule-aligned MAC checkpoint, and buffers
a pending program granule internally so it only ever calls `write()` with
`writeAlign`-aligned, forward, into-erased bytes - your `write()` is a simple
flash program (no buffering or read-modify-write in the port), `erase()`
erases the region (sector loop), `read()` reads raw bytes. A programmed cell
is never rewritten and a whole sector is erased only on compaction, so the
header sector is not worn on every append and a torn final commit is ignored
on the next load. Provide a `get_integrity_key` on `FWTPM_NV_HAL` so the
checkpoints authenticate the journal. (`writeAlign <= 1` disables buffering
for byte-writable NV like EEPROM/FRAM.)
2. **Clock HAL** (optional): `get_ms()` returning milliseconds since boot.
Register via `FWTPM_Clock_SetHAL()` before `FWTPM_Init()`.

View File

@ -238,6 +238,7 @@ static TPM_RC SwTpmConnect(TPM2_CTX* ctx, const char* host, const char* port)
speed_t baud;
int baudInt;
struct stat devStat;
int fdFlags;
if (ctx == NULL) {
return BAD_FUNC_ARG;
@ -245,7 +246,10 @@ static TPM_RC SwTpmConnect(TPM2_CTX* ctx, const char* host, const char* port)
/* Note: TPM2_SWTPM_HOST env var is checked by caller
* (TPM2_SWTPM_SendCommand) before invoking SwTpmConnect */
fd = open(host, O_RDWR | O_NOCTTY | O_CLOEXEC | O_NOFOLLOW);
/* Allow symlinked serial paths (no O_NOFOLLOW); S_ISCHR below rejects
* non-char-device swaps and O_NOCTTY avoids tty hijack. O_NONBLOCK keeps
* open() from hanging on a FIFO/no-carrier line; cleared after validation. */
fd = open(host, O_RDWR | O_NOCTTY | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) {
#ifdef DEBUG_WOLFTPM
printf("Failed to open UART device %s: %s\n", host, strerror(errno));
@ -264,6 +268,15 @@ static TPM_RC SwTpmConnect(TPM2_CTX* ctx, const char* host, const char* port)
close(fd);
return TPM_RC_FAILURE;
}
/* Restore blocking mode now that the target is a validated char device, so
* reads honor the termios VMIN/VTIME timeouts configured below. Fail hard if
* it cannot be cleared, rather than leaving a non-blocking fd for reads. */
fdFlags = fcntl(fd, F_GETFL);
if (fdFlags == -1 ||
fcntl(fd, F_SETFL, fdFlags & ~O_NONBLOCK) == -1) {
close(fd);
return TPM_RC_FAILURE;
}
/* Configure serial port: 8N1, raw mode, no flow control */
XMEMSET(&tty, 0, sizeof(tty));

View File

@ -10083,6 +10083,308 @@ static void test_fwtpm_nv_sethal_mock(void)
#endif
}
#if defined(WOLFTPM_FWTPM_NV_APPEND_ONLY) && !defined(FWTPM_NO_NV)
/* RAM-backed write-once flash on a plain FWTPM_NV_HAL. flash_write enforces
* aligned, into-erased programs so any in-place rewrite or double-program is
* caught. */
#define FNV_SIZE (64 * 1024)
#define FNV_SECTOR 4096
#define FNV_PROG 16
static byte gFlashNv[FNV_SIZE];
static int gFlashEraseCnt;
static int gFlashProgCnt;
static int flash_read(void* c, word32 off, byte* buf, word32 sz)
{
(void)c;
if ((size_t)off + sz > FNV_SIZE) {
return -1;
}
XMEMCPY(buf, gFlashNv + off, sz);
return 0;
}
static int flash_erase(void* c, word32 off, word32 sz)
{
(void)c;
if ((off % FNV_SECTOR) != 0 || (sz % FNV_SECTOR) != 0 || sz == 0 ||
(size_t)off + sz > FNV_SIZE) {
return -1;
}
XMEMSET(gFlashNv + off, 0xFF, sz);
gFlashEraseCnt++;
return 0;
}
static int flash_write(void* c, word32 off, const byte* buf, word32 sz)
{
word32 i;
(void)c;
/* Append-only contract: aligned program granules into erased cells. */
if ((off % FNV_PROG) != 0 || (sz % FNV_PROG) != 0 ||
(size_t)off + sz > FNV_SIZE) {
return -1;
}
for (i = 0; i < sz; i++) {
if (gFlashNv[off + i] != 0xFF) {
return -1; /* write-once violation */
}
}
XMEMCPY(gFlashNv + off, buf, sz);
gFlashProgCnt += sz / FNV_PROG;
return 0;
}
static int flash_key(void* c, byte* key, word32* keySz)
{
(void)c;
XMEMSET(key, 0xA5, 32);
*keySz = 32;
return 0;
}
/* Simulate a (re)boot: register the native read/write/erase HAL (opt in to
* append-only via flags + writeAlign) over the persistent flash buffer, then
* start the TPM. */
static int flash_boot(FWTPM_CTX* ctx, FWTPM_NV_HAL* hal, word32 align,
int useKey)
{
int rc;
XMEMSET(hal, 0, sizeof(*hal));
hal->read = flash_read;
hal->write = flash_write;
hal->erase = flash_erase;
hal->ctx = NULL;
hal->maxSize = FNV_SIZE;
hal->appendOnly = 1;
hal->writeAlign = align;
if (useKey) {
hal->get_integrity_key = flash_key;
}
XMEMSET(ctx, 0, sizeof(*ctx));
rc = FWTPM_NV_SetHAL(ctx, hal);
if (rc != 0) {
return rc;
}
return FWTPM_Init(ctx);
}
#endif /* WOLFTPM_FWTPM_NV_APPEND_ONLY && !FWTPM_NO_NV */
static void test_fwtpm_nv_flash_append_only(void)
{
#if defined(WOLFTPM_FWTPM_NV_APPEND_ONLY) && !defined(FWTPM_NO_NV)
FWTPM_CTX ctx;
FWTPM_NV_HAL hal;
byte savedSeed[FWTPM_SEED_SIZE];
byte bogus[FNV_PROG];
byte hdrBuf[sizeof(FWTPM_NV_HEADER)];
word32 tornAt;
word32 p1;
int rc;
int eraseBase;
int progBase;
int i;
int allZero;
/* TLV header (tag + length) is 4 bytes on the wire. */
const word32 tlvHdrSize = 4;
/* Start from fully erased flash. */
XMEMSET(gFlashNv, 0xFF, sizeof(gFlashNv));
gFlashEraseCnt = 0;
gFlashProgCnt = 0;
/* Boot 1: fresh init writes seeds via a compaction (erase + program). */
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
XMEMCPY(savedSeed, ctx.ownerSeed, FWTPM_SEED_SIZE);
AssertIntGT(gFlashProgCnt, 0);
AssertIntGT(gFlashEraseCnt, 0);
/* Targeted appends must NOT erase: append-only into pre-erased space. */
eraseBase = gFlashEraseCnt;
progBase = gFlashProgCnt;
ctx.disableClear = 1;
rc = FWTPM_NV_SaveFlags(&ctx);
AssertIntEQ(rc, 0);
rc = FWTPM_NV_SaveFlags(&ctx);
AssertIntEQ(rc, 0);
AssertIntEQ(gFlashEraseCnt, eraseBase); /* no erase on append */
AssertIntGT(gFlashProgCnt, progBase); /* but did program */
/* FWTPM_Cleanup compacts (erase + dense rewrite), so the next-append
* offset is only known after a reload. */
FWTPM_Cleanup(&ctx);
/* Boot 2: state replays from the journal. Capture the current next-append
* offset for the torn-commit simulation, then abandon this context WITHOUT
* the compacting cleanup so the on-flash layout (and that offset) stays
* valid. */
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
AssertIntEQ(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
AssertIntEQ((int)ctx.disableClear, 1);
tornAt = ctx.nvWritePos; /* next (aligned) append offset */
wc_FreeRng(&ctx.rng); /* drop ctx without a save */
/* Simulate a torn trailing commit: program bytes into the next erased
* granule with no valid checkpoint following. */
XMEMSET(bogus, 0x5A, sizeof(bogus));
rc = flash_write(NULL, tornAt, bogus, FNV_PROG);
AssertIntEQ(rc, 0);
/* Boot 3: the torn tail is ignored, committed state survives, and the
* recovery path rewrites cleanly (an erase happens). */
eraseBase = gFlashEraseCnt;
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
AssertIntEQ(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
AssertIntEQ((int)ctx.disableClear, 1);
AssertIntGT(gFlashEraseCnt, eraseBase); /* recovery compaction erased */
FWTPM_Cleanup(&ctx);
/* Tamper: flip a committed byte after the header. The checkpoint MAC must
* fail, so the journal is discarded and fresh seeds generated. */
gFlashNv[sizeof(FWTPM_NV_HEADER) + 1] ^= 0xFF;
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
AssertIntNE(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
FWTPM_Cleanup(&ctx);
/* writeAlign=32: header (16 B) and first entry share a program granule. */
XMEMSET(gFlashNv, 0xFF, sizeof(gFlashNv));
gFlashEraseCnt = 0; gFlashProgCnt = 0;
rc = flash_boot(&ctx, &hal, 32, 1);
AssertIntEQ(rc, 0);
XMEMCPY(savedSeed, ctx.ownerSeed, FWTPM_SEED_SIZE);
ctx.disableClear = 1;
rc = FWTPM_NV_SaveFlags(&ctx);
AssertIntEQ(rc, 0);
FWTPM_Cleanup(&ctx);
rc = flash_boot(&ctx, &hal, 32, 1);
AssertIntEQ(rc, 0);
AssertIntEQ(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
AssertIntEQ((int)ctx.disableClear, 1);
FWTPM_Cleanup(&ctx);
/* No integrity key: load follows the "trust the scan" path. */
XMEMSET(gFlashNv, 0xFF, sizeof(gFlashNv));
gFlashEraseCnt = 0; gFlashProgCnt = 0;
rc = flash_boot(&ctx, &hal, FNV_PROG, 0);
AssertIntEQ(rc, 0);
XMEMCPY(savedSeed, ctx.ownerSeed, FWTPM_SEED_SIZE);
ctx.disableClear = 1;
rc = FWTPM_NV_SaveFlags(&ctx);
AssertIntEQ(rc, 0);
FWTPM_Cleanup(&ctx);
rc = flash_boot(&ctx, &hal, FNV_PROG, 0);
AssertIntEQ(rc, 0);
AssertIntEQ(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
AssertIntEQ((int)ctx.disableClear, 1);
FWTPM_Cleanup(&ctx);
/* Fill the journal to force an in-place append-only compaction, then
* confirm the latest state survives a reload. */
XMEMSET(gFlashNv, 0xFF, sizeof(gFlashNv));
gFlashEraseCnt = 0; gFlashProgCnt = 0;
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
XMEMCPY(savedSeed, ctx.ownerSeed, FWTPM_SEED_SIZE);
eraseBase = gFlashEraseCnt;
for (i = 0; i < 2000; i++) {
ctx.disableClear = (i & 1);
rc = FWTPM_NV_SaveFlags(&ctx);
AssertIntEQ(rc, 0);
}
AssertIntGT(gFlashEraseCnt, eraseBase); /* compaction(s) occurred */
FWTPM_Cleanup(&ctx);
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
AssertIntEQ(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
AssertIntEQ((int)ctx.disableClear, 1); /* last write was i=1999 -> 1 */
FWTPM_Cleanup(&ctx);
/* Anti-rollback: two committed checkpoints, then tamper a byte covered by
* the later checkpoint (but after the first). The loader must reject and
* regenerate fresh state rather than silently rolling back to the first
* checkpoint. */
XMEMSET(gFlashNv, 0xFF, sizeof(gFlashNv));
gFlashEraseCnt = 0; gFlashProgCnt = 0;
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
XMEMCPY(savedSeed, ctx.ownerSeed, FWTPM_SEED_SIZE);
ctx.disableClear = 1;
rc = FWTPM_NV_SaveFlags(&ctx); /* checkpoint 1 */
AssertIntEQ(rc, 0);
p1 = ctx.nvWritePos; /* aligned end of checkpoint 1 */
ctx.disableClear = 0;
rc = FWTPM_NV_SaveFlags(&ctx); /* checkpoint 2 */
AssertIntEQ(rc, 0);
wc_FreeRng(&ctx.rng); /* drop ctx, keep flash layout */
/* Flip a byte in the second FLAGS entry value: covered by checkpoint 2,
* not by checkpoint 1, so checkpoint 1 stays valid. */
gFlashNv[p1 + tlvHdrSize] ^= 0xFF;
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
AssertIntNE(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
FWTPM_Cleanup(&ctx);
/* Partial-header torn write at the append offset: tag programmed but length
* still erased (0xFFFF). The scan must treat it as a dirty tail, recover
* committed state, compact, and leave the append offset writable. */
XMEMSET(gFlashNv, 0xFF, sizeof(gFlashNv));
gFlashEraseCnt = 0; gFlashProgCnt = 0;
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
XMEMCPY(savedSeed, ctx.ownerSeed, FWTPM_SEED_SIZE);
ctx.disableClear = 1;
rc = FWTPM_NV_SaveFlags(&ctx);
AssertIntEQ(rc, 0);
FWTPM_Cleanup(&ctx);
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
tornAt = ctx.nvWritePos; /* next (aligned) append offset */
wc_FreeRng(&ctx.rng); /* drop ctx without a save */
XMEMSET(bogus, 0x5A, sizeof(bogus));
bogus[0] = 0x30; bogus[1] = 0x00; /* non-FREE tag (FLAGS) */
bogus[2] = 0xFF; bogus[3] = 0xFF; /* length still erased */
rc = flash_write(NULL, tornAt, bogus, FNV_PROG);
AssertIntEQ(rc, 0);
eraseBase = gFlashEraseCnt;
rc = flash_boot(&ctx, &hal, FNV_PROG, 1);
AssertIntEQ(rc, 0);
AssertIntEQ(XMEMCMP(ctx.ownerSeed, savedSeed, FWTPM_SEED_SIZE), 0);
AssertIntEQ((int)ctx.disableClear, 1);
AssertIntGT(gFlashEraseCnt, eraseBase); /* dirty tail compacted */
rc = FWTPM_NV_SaveFlags(&ctx); /* append offset is writable */
AssertIntEQ(rc, 0);
FWTPM_Cleanup(&ctx);
/* No integrity key, valid header but no checkpoint: an uncheckpointed image
* must be treated as uninitialized and regenerate fresh seeds, not loaded
* as committed empty state. */
XMEMSET(gFlashNv, 0xFF, sizeof(gFlashNv));
gFlashEraseCnt = 0; gFlashProgCnt = 0;
FwStoreU32LE(hdrBuf + 0, FWTPM_NV_MAGIC);
FwStoreU32LE(hdrBuf + 4, FWTPM_NV_VERSION);
FwStoreU32LE(hdrBuf + 8, (UINT32)sizeof(FWTPM_NV_HEADER));
FwStoreU32LE(hdrBuf + 12, FNV_SIZE);
rc = flash_write(NULL, 0, hdrBuf, sizeof(hdrBuf));
AssertIntEQ(rc, 0);
rc = flash_boot(&ctx, &hal, FNV_PROG, 0);
AssertIntEQ(rc, 0);
allZero = 1;
for (i = 0; i < FWTPM_SEED_SIZE; i++) {
if (ctx.ownerSeed[i] != 0) {
allZero = 0;
break;
}
}
AssertIntEQ(allZero, 0); /* fresh seeds were generated */
FWTPM_Cleanup(&ctx);
fwtpm_pass("NV append-only flash "
"(persist/torn/tamper/rollback/no-key/compact):", 0);
#else
printf("Test fwTPM: %-6s %-42s Skipped\n", "",
"NV append-only flash:");
#endif
}
/* ================================================================== */
/* main */
/* ================================================================== */
@ -10150,6 +10452,7 @@ int fwtpm_unit_tests(int argc, char *argv[])
* state, so remove FWTPM_NV_FILE afterwards. */
test_fwtpm_clock_sethal();
test_fwtpm_nv_sethal_mock();
test_fwtpm_nv_flash_append_only();
(void)remove(FWTPM_NV_FILE);
/* Key operations */

View File

@ -622,6 +622,11 @@ struct FWTPM_NV_HAL_S {
* success. When NULL the default file backend uses an auto-created key
* file; integrity verification is always performed when a key exists. */
int (*get_integrity_key)(void* ctx, byte* key, word32* keySz);
/* Program granule in bytes for append-only flash (0/1 = byte-writable). */
word32 writeAlign;
/* Append-only (write-once flash) mode; needs WOLFTPM_FWTPM_NV_APPEND_ONLY.
* Remaining bits reserved. */
unsigned int appendOnly : 1;
};
/* Clock HAL callbacks (optional - if not set, clockOffset used directly) */
@ -630,6 +635,13 @@ struct FWTPM_CLOCK_HAL_S {
void* ctx;
};
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
/* Max append-only program granule; sizes the pending-granule buffer. */
#ifndef FWTPM_NV_MAX_WRITE_ALIGN
#define FWTPM_NV_MAX_WRITE_ALIGN 64
#endif
#endif
/* fwTPM context - holds all TPM state */
typedef struct FWTPM_CTX {
volatile int running; /* Server running flag (volatile for signal handler) */
@ -747,6 +759,15 @@ typedef struct FWTPM_CTX {
word32 nvWritePos;
int nvCompacting; /* Guard flag to prevent cyclic recursion during NV compaction */
#ifdef WOLFTPM_FWTPM_NV_APPEND_ONLY
/* Append-only pending program granule (word-backed for alignment; element
* count rounded up so an odd FWTPM_NV_MAX_WRITE_ALIGN override still fits). */
word32 nvGranule[(FWTPM_NV_MAX_WRITE_ALIGN + sizeof(word32) - 1)
/ sizeof(word32)];
word32 nvGranuleBase; /* aligned offset of the pending granule */
word32 nvGranuleFill; /* bytes buffered (0..writeAlign) */
#endif
/* ContextSave sequence counter (monotonic, reset on init) */
UINT64 contextSeqCounter;
/* Live (saved-but-not-yet-loaded) context sequences. A context loads at

View File

@ -90,6 +90,10 @@ typedef struct FWTPM_NV_HEADER {
#define FWTPM_NV_TAG_FREE 0xFFFF /* Erased flash */
#define FWTPM_NV_TAG_INVALID 0x0000 /* Sentinel/deleted */
/* Append-only MAC checkpoint: HMAC over the body before it + 0xFF pad in the
* value; skipped on replay. */
#define FWTPM_NV_TAG_MAC 0x00F0
/* Hierarchy seeds (48 bytes each) */
#define FWTPM_NV_TAG_OWNER_SEED 0x0001
#define FWTPM_NV_TAG_ENDORSEMENT_SEED 0x0002
@ -176,7 +180,11 @@ WOLFTPM_API int FWTPM_NV_Save(FWTPM_CTX* ctx);
\param ctx pointer to an initialized FWTPM_CTX
\param hal pointer to a caller-populated FWTPM_NV_HAL (the struct
must remain valid for the lifetime of the context)
must remain valid for the lifetime of the context). The caller MUST
zero-initialize the struct (e.g. XMEMSET(&hal, 0, sizeof(hal)))
before populating fields: the appendOnly bit-field and reserved bits
must be 0 unless append-only mode is intentionally enabled, otherwise
an indeterminate appendOnly bit can select write-once behavior.
\sa FWTPM_NV_Init
*/