A tamper-evident audit log in two pages of flash
How OpenHSM records every operation in a 32 KB ring on an STM32U585, chains the entries with truncated HMACs, and lets the host verify the chain.
An HSM is only as trustworthy as your ability to find out what it has done. OpenHSM keeps an audit log of every operation, and has to fit it inside the constraints of a small microcontroller: no filesystem, no real-time clock, flash that can only be erased a page at a time, and about 19 KB of RAM in total.
Layout
The log uses two 8 KB flash pages in bank 2 as a ring of 1024 entries of 16 bytes. Sixteen bytes is the STM32U5’s flash programming unit (a quad-word), so each entry is written in a single program operation and is never partially updated.
Writes only ever append. When the ring wraps, only the page about to be reused is erased, so the other page, holding the most recent history, survives the erase.
The chain
Each entry carries a monotonic sequence number, the event, an argument, and a MAC:
mac = HMAC-SHA256(audit_key, prev_mac ‖ seq ‖ event ‖ arg)[0:8]
The audit key is derived from the device KEK with HKDF, so it never exists outside the device unless someone explicitly asks for it. Because every MAC covers the previous one, changing, removing or reordering any entry breaks every MAC after it.
The MAC is truncated to 8 bytes because the entry has to fit in 16. An attacker would have to forge each truncated MAC online against the device’s key, which is a very different problem from brute-forcing a hash offline.
Recovery and verification
There’s no RAM state that survives a reset, so at boot the firmware rescans the two pages, finds the highest sequence number, and resumes the chain from that entry’s MAC.
Verification happens on the host:
openhsm-cli audit-verify
The CLI fetches the audit key through a gated command, reads the entries and recomputes the chain. Gaps in the sequence numbers are expected, because the ring evicts old entries. The verifier treats each gap as an eviction boundary rather than an attack, and checks the chain inside each continuous run.
What it costs
Writing an entry for every operation costs about 25% of throughput. On this hardware that means AES-GCM at around 850 operations per second and HMAC at around 620. For a device whose main job is unsealing a secrets manager, that is cheap.
What it doesn’t do yet
The README is explicit about the gaps, and they matter for how far you can trust the log:
- No timestamps. There is no RTC, so entries are ordered by sequence number, not by time.
- Unplugging counts as tamper. Every unplug currently logs a
TAMPERevent, which dilutes real tamper signals. - The key is only as secret as the KEK. The KEK is still derived from the chip’s readable UID. Until readout protection, TrustZone and a provisioned secret are in place, someone with physical access to the chip could derive the audit key and rewrite the log consistently.
The chain is the right structure, and the remaining work is making its key genuinely secret. The roadmap orders that work from safe to irreversible, because on a board without a debugger a wrong TrustZone setting costs you a mass erase.