fwTPM: append-only NV journal for write-once flash + swtpm UART symlinks#540
fwTPM: append-only NV journal for write-once flash + swtpm UART symlinks#540dgarske wants to merge 1 commit into
Conversation
…RT symlink support
47e2ee4 to
724ffdf
Compare
aidangarske
left a comment
There was a problem hiding this comment.
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 checkpoint —
src/fwtpm/fwtpm_nv.c:1517-1604 - [High] [bugs] Append-only recovery misses a torn header at the next append offset —
src/fwtpm/fwtpm_nv.c:1514-1604 - [Medium] [review] Document zero-initialization requirement for extended NV HAL —
wolftpm/fwtpm/fwtpm_nv.h:182-184 - [Medium] [review] Add coverage for no-key append-only images without a checkpoint —
src/fwtpm/fwtpm_nv.c:1545-1557
Review generated by Skoll
| if (scanPos + TLV_HDR_SIZE + len > hal->maxSize) { | ||
| break; /* truncated trailing entry */ | ||
| } | ||
| if (tag == FWTPM_NV_TAG_MAC) { |
There was a problem hiding this comment.
🔴 [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.
| if (tag == FWTPM_NV_TAG_FREE) { | ||
| break; /* erased space: end of journal */ | ||
| } | ||
| if (scanPos + TLV_HDR_SIZE + len > hal->maxSize) { |
There was a problem hiding this comment.
🔴 [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).
| \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) |
There was a problem hiding this comment.
🟠 [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.
| TPM2_ForceZero(vKey, sizeof(vKey)); | ||
| } | ||
|
|
||
| if (haveKey && lastMacEnd == 0) { |
There was a problem hiding this comment.
🟠 [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.
writePosby 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 existingFWTPM_NV_HAL(read/write/erase) - a port setshal.appendOnly = 1andhal.writeAlign; no separate adapter. Opt-in and off by default (--enable-fwtpm-nv-appendonly/ CMakeWOLFTPM_FWTPM_NV_APPEND_ONLY); the file backend and default builds are unchanged and carry no extra memory.O_NOFOLLOW; keepsS_ISCHR/O_NOCTTY, addsO_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.mdandsrc/fwtpm/ports/README.md(links wolftpm-examples; ports: STM32H5, PolarFire SoC U54, ZCU102 R5 lock-step).