Skip to content

fwTPM: append-only NV journal for write-once flash + swtpm UART symlinks#540

Open
dgarske wants to merge 1 commit into
wolfSSL:masterfrom
dgarske:fwtpm_flash_nv_and_uart_symlink
Open

fwTPM: append-only NV journal for write-once flash + swtpm UART symlinks#540
dgarske wants to merge 1 commit into
wolfSSL:masterfrom
dgarske:fwtpm_flash_nv_and_uart_symlink

Conversation

@dgarske

@dgarske dgarske commented Jun 24, 2026

Copy link
Copy Markdown
Member
  • Append-only NV mode for write-once flash (internal flash/NOR). The journal stops rewriting the header and trailing MAC in place; instead it derives writePos by scanning, seals each commit with an appended MAC checkpoint, and streams granule-aligned writes so a sector is erased only on compaction. This avoids wearing the header sector and survives a torn final commit (power loss). It plugs into the existing FWTPM_NV_HAL (read/write/erase) - a port sets hal.appendOnly = 1 and hal.writeAlign; no separate adapter. Opt-in and off by default (--enable-fwtpm-nv-appendonly / CMake WOLFTPM_FWTPM_NV_APPEND_ONLY); the file backend and default builds are unchanged and carry no extra memory.
  • swtpm UART now accepts symlinked serial paths (drops O_NOFOLLOW; keeps S_ISCHR/O_NOCTTY, adds O_NONBLOCK).

Tests: new unit test (RAM-backed write-once HAL) covers persist/reboot, torn-tail recovery, and tamper rejection. Verified clean with the option both off and on.

Docs: docs/FWTPM.md and src/fwtpm/ports/README.md (links wolftpm-examples; ports: STM32H5, PolarFire SoC U54, ZCU102 R5 lock-step).

@dgarske dgarske self-assigned this Jun 24, 2026
Copilot AI review requested due to automatic review settings June 24, 2026 22:37
@dgarske dgarske marked this pull request as ready for review June 25, 2026 17:15
@dgarske dgarske force-pushed the fwtpm_flash_nv_and_uart_symlink branch from 47e2ee4 to 724ffdf Compare June 25, 2026 18:18
@dgarske dgarske removed their assignment Jun 25, 2026

@aidangarske aidangarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skoll Multi-Scan Review

Modes: review + review-security + bugsOverall recommendation: REQUEST_CHANGES
Findings: 4 total — 4 posted, 0 skipped
4 finding(s) posted as inline comments (see file-level comments below)

Posted findings

  • [High] [review-security] Append-only NV tamper can roll state back to an earlier checkpointsrc/fwtpm/fwtpm_nv.c:1517-1604
  • [High] [bugs] Append-only recovery misses a torn header at the next append offsetsrc/fwtpm/fwtpm_nv.c:1514-1604
  • [Medium] [review] Document zero-initialization requirement for extended NV HALwolftpm/fwtpm/fwtpm_nv.h:182-184
  • [Medium] [review] Add coverage for no-key append-only images without a checkpointsrc/fwtpm/fwtpm_nv.c:1545-1557

Review generated by Skoll

Comment thread src/fwtpm/fwtpm_nv.c
if (scanPos + TLV_HDR_SIZE + len > hal->maxSize) {
break; /* truncated trailing entry */
}
if (tag == FWTPM_NV_TAG_MAC) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [High] Append-only NV tamper can roll state back to an earlier checkpoint · Security

Before this PR, keyed NV integrity used one trailing MAC over the whole journal, and FWTPM_NV_Init treated any MAC mismatch as TPM_RC_NV_UNINITIALIZED, discarding tampered state. The append-only loader now scans for the last checkpoint whose MAC verifies and, when later data or checkpoints fail authentication, replays the earlier checkpoint and compacts it. A storage attacker who can flip/program a bit in flash after checkpoint N can make checkpoint N+1 and all later checkpoints fail, causing the TPM to boot from checkpoint N instead of rejecting the journal. That can roll back persistent TPM state such as hierarchy auth changes, disableClear, NV index updates, persistent objects, or clock/reset state without knowing the integrity key. The added test only flips data in a compacted single-checkpoint journal, so it exercises fresh-state rejection when lastMacEnd == 0, but not tamper after a prior valid checkpoint.

Fix: In keyed append-only mode, distinguish torn/incomplete tail recovery from authenticated tamper. At minimum, track full MAC checkpoint entries that are structurally present but fail verification after a previous valid checkpoint and return TPM_RC_NV_UNINITIALIZED instead of compacting the older state. Consider authenticating checkpoint metadata and adding a monotonic checkpoint sequence/anti-rollback policy. Add a unit test with at least two committed checkpoints, then tamper the later committed entry or checkpoint and assert tamper rejection rather than rollback.

Comment thread src/fwtpm/fwtpm_nv.c
if (tag == FWTPM_NV_TAG_FREE) {
break; /* erased space: end of journal */
}
if (scanPos + TLV_HDR_SIZE + len > hal->maxSize) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [High] Append-only recovery misses a torn header at the next append offset · Logic Errors

The PR adds append-only torn-tail recovery, but it only compacts when scanPos > lastMacEnd. If power fails while programming the first bytes of the next TLV header after a valid checkpoint, the header can be non-free but have an erased or oversized length, for example tag bytes programmed and length still 0xFFFF. The scan breaks as a truncated trailing entry while scanPos == lastMacEnd, so recovery does not erase the dirty bytes. The context then resumes with ctx->nvWritePos = lastMacEnd; the next append tries to program over those already-programmed bytes and the write-once HAL rejects it. The new test writes a full 0x5A granule, which advances scanPos, but it does not cover this partial-header torn-write case.

Fix: Track whether the scan encountered any non-free uncommitted bytes after the last valid checkpoint, including a truncated entry at lastMacEnd, and compact when that flag is set (e.g. set a dirtyTail flag on a truncated trailing entry and trigger FWTPM_NV_Save when dirtyTail is set rather than only when scanPos > lastMacEnd).

Comment thread wolftpm/fwtpm/fwtpm_nv.h
\return BAD_FUNC_ARG if ctx or hal is NULL

\param ctx pointer to an initialized FWTPM_CTX
\param ctx pointer to a zeroed FWTPM_CTX (before FWTPM_Init)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [Medium] Document zero-initialization requirement for extended NV HAL · api

The PR adds writeAlign and the appendOnly bit to the public FWTPM_NV_HAL struct, and FWTPM_NV_SetHAL() copies the caller's struct as-is. Existing callers that populate only the pre-existing callbacks/size fields without first zeroing the struct can now pass an indeterminate appendOnly bit when WOLFTPM_FWTPM_NV_APPEND_ONLY is compiled in, accidentally selecting append-only behavior. The new docs show XMEMSET(&hal, 0, sizeof(hal)), but the public API contract here still says only caller-populated, so the new field makes zero-initialization a real correctness requirement.

Fix: Update the Doxygen/API docs to explicitly require zero-initializing FWTPM_NV_HAL before setting fields, or provide a small initializer helper/macro for new HAL instances so ports cannot accidentally enable append-only mode through an uninitialized bit-field.

Comment thread src/fwtpm/fwtpm_nv.c
TPM2_ForceZero(vKey, sizeof(vKey));
}

if (haveKey && lastMacEnd == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [Medium] Add coverage for no-key append-only images without a checkpoint · test

The append-only loader requires a checkpoint before accepting the journal only when an integrity key exists. In the no-key path it trusts the scan even if lastMacEnd == 0, so an image with a valid header but no completed checkpoint is treated as successfully loaded. Since append-only compaction writes the header before the final checkpoint, a no-key power loss during that window can leave a header-only or partially rewritten image that reloads as committed state. The new tests cover the no-key happy path, but not this uncheckpointed/no-key edge.

Fix: Add a test that boots with useKey=0 from an image containing a valid append-only header but no FWTPM_NV_TAG_MAC checkpoint, and assert the intended behavior. If checkpoint tags are intended to be commit markers even without a MAC, reject lastMacEnd == 0 for all append-only loads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants