HAL and Porting¶
The fwTPM provides hardware abstraction layers (HALs) so it can be ported to embedded targets without modifying the core logic. There is an IO HAL for the transport, an NV HAL for persistent storage, and an optional clock HAL. Full reference ports for several boards live in the wolftpm-examples repository.
IO HAL (Transport)¶
The IO HAL abstracts the transport between the fwTPM server and its clients. The default implementation uses TCP sockets (SWTPM protocol). For embedded targets, replace it with SPI, I2C, UART, or shared memory callbacks.
Callback structure (defined as FWTPM_IO_HAL in fwtpm.h):
| Callback | Signature | Description |
|---|---|---|
send |
int (*)(void* ctx, const void* buf, int sz) |
Send data to client |
recv |
int (*)(void* ctx, void* buf, int sz) |
Receive data from client |
wait |
int (*)(void* ctx) |
Wait for data or connections. Returns a bitmask: 0x01=command data, 0x02=platform data, 0x04=new command connection, 0x08=new platform connection |
accept |
int (*)(void* ctx, int type) |
Accept new connection (type: 0=command, 1=platform) |
close_conn |
void (*)(void* ctx, int type) |
Close connection (type: 0=command, 1=platform) |
ctx |
void* |
User context pointer |
Registration:
FWTPM_IO_HAL myHal;
myHal.send = my_send;
myHal.recv = my_recv;
myHal.wait = my_wait;
myHal.accept = my_accept;
myHal.close_conn = my_close;
myHal.ctx = &myTransportCtx;
FWTPM_IO_SetHAL(&ctx, &myHal);
NV HAL (Persistent Storage)¶
The NV HAL abstracts persistent storage. The default implementation uses a local file (fwtpm_nv.bin). For embedded targets, replace it with flash, EEPROM, or other non-volatile storage callbacks.
Callback structure (defined as FWTPM_NV_HAL in fwtpm.h):
| Callback | Signature | Description |
|---|---|---|
read |
int (*)(void* ctx, word32 offset, byte* buf, word32 size) |
Read from NV at offset |
write |
int (*)(void* ctx, word32 offset, const byte* buf, word32 size) |
Write to NV at offset |
erase |
see fwtpm.h |
Erase the NV region (used by flash ports and by compaction) |
ctx |
void* |
User context pointer |
maxSize |
word32 |
Size of the NV region in bytes |
appendOnly, writeAlign |
fields | Append-only mode and program granule size (see below) |
get_integrity_key |
callback | Supplies a device secret used to authenticate the journal |
Registration:
FWTPM_NV_HAL myNvHal;
myNvHal.read = my_flash_read;
myNvHal.write = my_flash_write;
myNvHal.ctx = &myFlashCtx;
FWTPM_NV_SetHAL(&ctx, &myNvHal);
Register the HAL before FWTPM_Init().
NV Storage HAL for Embedded Ports¶
NV access goes through FWTPM_NV_HAL (read, write, erase, ctx, maxSize, get_integrity_key). The default backend is a file. 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 aligned to the program granularity, so they cannot service in-place rewrites.
For those parts, build with --enable-fwtpm-nv-appendonly (-DWOLFTPM_FWTPM_NV_APPEND_ONLY, CMake WOLFTPM_FWTPM_NV_APPEND_ONLY=yes) and set appendOnly and writeAlign on the HAL. The journal then runs in append-only mode:
- The header is written only at compaction.
writePosis derived by scanning on load.- Each commit is sealed with an appended MAC-checkpoint entry, padded up to
writeAlign.
The existing read, write, and erase HAL is the integration point. There is no separate adapter.
/* 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(). 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 (for example from 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. Setting writeAlign <= 1 selects no buffering, so byte-writable NV (EEPROM or FRAM) works with a plain write().
Warning
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.
Clock HAL¶
The clock HAL is optional. It supplies get_ms(), which returns milliseconds since boot. Register it with FWTPM_Clock_SetHAL() before FWTPM_Init(). With a clock HAL registered, dictionary attack protection self-heals over time (see Overview).
Porting Example¶
A bare-metal embedded target with SPI transport and SPI flash NV:
FWTPM_CTX ctx;
XMEMSET(&ctx, 0, sizeof(ctx));
/* Set custom NV storage before FWTPM_Init, which loads NV state through it */
FWTPM_NV_HAL nvHal = {
.read = spi_flash_read,
.write = spi_flash_write,
.ctx = &flashHandle
};
FWTPM_NV_SetHAL(&ctx, &nvHal);
FWTPM_Init(&ctx);
/* Set custom IO transport after FWTPM_Init, which does not preserve the IO HAL */
FWTPM_IO_HAL ioHal = {
.send = spi_slave_send,
.recv = spi_slave_recv,
.wait = spi_slave_poll,
.accept = NULL, /* not connection-oriented */
.close_conn = NULL,
.ctx = &spiHandle
};
FWTPM_IO_SetHAL(&ctx, &ioHal);
/* Initialize IO and run */
FWTPM_IO_Init(&ctx);
FWTPM_IO_ServerLoop(&ctx); /* blocks */
FWTPM_IO_Cleanup(&ctx);
FWTPM_Cleanup(&ctx);
Available Ports¶
| Port | Repository | Description |
|---|---|---|
| STM32H5 | STM32/fwtpm-stm32h5 | STM32H5 Cortex-M33 with TrustZone (CMSE); internal-flash NV |
| PolarFire SoC | 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 | 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¶
To add a new platform, implement these HAL callbacks:
-
NV Storage HAL (
FWTPM_NV_HAL):read(),write(), anderase()for persistent flash storage. Register it withFWTPM_NV_SetHAL()beforeFWTPM_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 aligned to the program granularity, so they cannot service in-place rewrites. For those, build with
--enable-fwtpm-nv-appendonlyand sethal.appendOnly = 1andhal.writeAlign = <program size>(for example 16 on STM32H5) beforeFWTPM_NV_SetHAL().The journal then writes the header only at compaction, derives
writePosby scanning on load, seals each commit with an appended, program-granule-aligned MAC checkpoint, and buffers a pending program granule internally. It only ever callswrite()withwriteAlign-aligned, forward, into-erased bytes. Yourwrite()is therefore a simple flash program with no buffering or read-modify-write in the port,erase()erases the region (sector loop), andread()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 aget_integrity_keyonFWTPM_NV_HALso the checkpoints authenticate the journal. SettingwriteAlign <= 1disables buffering for byte-writable NV such as EEPROM or FRAM. -
Clock HAL (optional):
get_ms()returning milliseconds since boot. Register it withFWTPM_Clock_SetHAL()beforeFWTPM_Init(). -
Entry point: zero
FWTPM_CTX, register the HALs, callFWTPM_Init(), then process TPM commands withFWTPM_ProcessCommand().
See the STM32 port in wolftpm-examples for a complete reference implementation.