Skip to content

HAL and Porting

The fwTPM provides hardware abstraction layers (HALs) for the server transport, persistent storage, and the platform clock. 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. Socket builds use TCP sockets (SWTPM protocol) by default. Registering an FWTPM_IO_HAL replaces the socket transport with callbacks for an embedded or in-process transport. TIS builds use their separate TIS HAL.

Callback structure (defined as FWTPM_IO_HAL in fwtpm.h):

Callback Signature Description
send int (*)(void* ctx, int type, const void* buf, int sz) Send data to the command or platform client
recv int (*)(void* ctx, int type, void* buf, int sz) Receive data from the command or platform client
wait int (*)(void* ctx, int timeoutMs) Wait up to timeoutMs for data or connections. Returns 0 on timeout or 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

The type argument is FWTPM_IO_TYPE_COMMAND or FWTPM_IO_TYPE_PLATFORM. The send, recv, and wait callbacks are required. wait must return by its requested timeout so stop requests are observed; override FWTPM_IO_WAIT_TIMEOUT_MS to change the default 1000 ms interval. Connectionless transports may omit accept and close_conn; a transport that provides accept must also provide close_conn and clean up any partial connection state before returning an accept error.

Registration:

FWTPM_IO_HAL myHal;
XMEMSET(&myHal, 0, sizeof(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);

Register the IO HAL before FWTPM_Init(), or after FWTPM_Init() but before FWTPM_IO_Init().

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.
  • writePos is 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 target can register SPI transport and SPI flash NV callbacks before initialization:

FWTPM_CTX ctx;
int cleanupRc;
int rc;
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);

/* Register a connectionless IO transport before initialization. */
FWTPM_IO_HAL ioHal = {
    .send = spi_slave_send,
    .recv = spi_slave_recv,
    .wait = spi_slave_poll,
    .accept = NULL,
    .close_conn = NULL,
    .ctx = &spiHandle
};
FWTPM_IO_SetHAL(&ctx, &ioHal);

rc = FWTPM_Init(&ctx);
if (rc == 0) {
    rc = FWTPM_IO_Init(&ctx);
    if (rc == 0) {
        rc = FWTPM_IO_ServerLoop(&ctx);  /* blocks */
        FWTPM_IO_Cleanup(&ctx);
    }
    cleanupRc = FWTPM_Cleanup(&ctx);
    if (rc == 0)
        rc = cleanupRc;
}
return rc;

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:

  1. NV Storage HAL (FWTPM_NV_HAL): read(), write(), and erase() for persistent flash storage. Register it with 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 aligned to the program granularity, so they cannot service in-place rewrites. For those, build with --enable-fwtpm-nv-appendonly and set hal.appendOnly = 1 and hal.writeAlign = <program size> (for example 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. It only ever calls write() with writeAlign-aligned, forward, into-erased bytes. Your write() is therefore a simple flash program with no buffering or read-modify-write in the port, erase() erases the region (sector loop), and 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. Setting writeAlign <= 1 disables buffering for byte-writable NV such as EEPROM or FRAM.

  2. Clock HAL (optional): get_ms() returning milliseconds since boot. Register it with FWTPM_Clock_SetHAL() before FWTPM_Init().

  3. Entry point: zero FWTPM_CTX, register the HALs, call FWTPM_Init(), then process TPM commands with FWTPM_ProcessCommand().

See the STM32 port in wolftpm-examples for a complete reference implementation.

See Also