|
|
||
|---|---|---|
| .. | ||
| host_test | ||
| Makefile | ||
| README.md | ||
| linker.ld | ||
| main.c | ||
| main_interactive.c | ||
| startup.c | ||
| stm32.c | ||
| user_settings.h | ||
README.md
wolfCrypt SRAM PUF Example
Bare-metal example demonstrating SRAM PUF (Physically Unclonable Function) on Cortex-M targets (tested on NUCLEO-H563ZI).
Overview
SRAM PUF exploits the random power-on state of SRAM memory cells to derive device-unique cryptographic keys. Each chip has a unique SRAM "fingerprint" caused by manufacturing variations.
This example demonstrates:
- Enrollment - Read raw SRAM, generate helper data using BCH(127,64,t=10) error-correcting codes
- Reconstruction - Re-read (noisy) SRAM, use helper data to recover the same stable bits despite bit flips (corrects up to 10 per 127-bit codeword)
- Key derivation - Use HKDF-SHA256 to derive a 256-bit cryptographic key
- Device identity - SHA-256 hash of stable bits serves as a unique device ID
Building
Requirements
arm-none-eabi-gcctoolchain
Test Mode (default)
Uses synthetic SRAM data -- runs on any target, no real hardware needed:
make
Real Hardware Mode
Test mode is the default and is selected by the Makefile (not the
header). To build for the real SRAM PUF on hardware, override the
PUF_TEST variable:
make PUF_TEST=0
This drops the -DWOLFSSL_PUF_TEST define and includes puf_sram_region
(placed in the .puf_sram NOLOAD section) so wc_PufReadSram() reads
the real power-on SRAM contents.
Only a real power cycle gives a real readout. A warm reset - the reset
button, a debugger reset, or -rst after flashing - leaves SRAM holding
whatever the previous image left there. That stale content can still pass the
Hamming-weight health band, so the example will happily enroll from it and
report a plausible-looking identity that has nothing to do with the silicon.
Pull power (or unplug USB) between enrollment and reconstruction when you want
to exercise the PUF itself.
Measured on a NUCLEO-H563ZI: a cold-boot readout is about 51-52% ones, well
inside the default 35-65% band, and reconstruction recovers the enrolled
identity unchanged across a physical power cycle - so this part's SRAM noise
stays within the BCH t=10 correction budget. Immediately after a warm reset the
same board reported 20% ones and was correctly rejected with PUF_READ_E.
Interactive Mode
The interactive demo has a host-side behavioral test: host_test/ builds
main_interactive.c against a stdio HAL and driver.py drives the menu end
to end (enrollment, the sweep gate and correction cliff, blob dump and
recovery, checksum and identity-mismatch rejection, paste abort, and the
fail-closed unhealthy-readout path). CI runs it on every change; locally:
make -C host_test WOLFSSL_ROOT=/path/to/wolfssl run.
make INTERACTIVE=1
Requires wolfSSL master (the demo uses PUF APIs added after v5.9.2; the build
stops with a clear #error on older trees). Output goes to
Build-interactive/ so switching modes never reuses stale objects.
Derived keys are never written to the UART by default - the console is an
unauthenticated physical interface, so the demo prints a "derived OK (not
shown)" status instead. For lab work where seeing the key bytes matters,
make INTERACTIVE=1 SHOW_KEYS=1 opts in explicitly.
If the power-on readout fails the Hamming-weight health band (which is what a warm reset looks like, since SRAM keeps the previous image's data), the demo fails closed: enrollment, reconstruction, and key derivation are disabled until a genuine power cycle provides a real readout. Nothing is ever derived from a substitute pattern.
Builds main_interactive.c instead of the one-shot example: a UART menu that
captures the real power-on SRAM at reset, reports whether it passed the readout
health band, and then lets you drive the extractor a step at a time.
=== wolfCrypt PUF - interactive demo ===
profile : BCH(127,64,t=10) over GF(2^7), 16 codewords, id 0x38500010
power-on SRAM readout: 256 bytes, 44% ones -> inside the health band
[1] enroll and show identity / key / helper
[2] noise sweep - the correction cliff
[3] two keys from one PUF
[4] dump the public recovery blob (identity + helper)
[5] paste the blob back after a power cycle, and verify
[r] reboot (soft reset - SRAM is NOT re-randomised)
Option 2 is the interesting one: it injects a known number of bit flips per codeword and shows exactly where BCH stops correcting.
flips/codeword result
9 identity matches
10 identity matches <= t, the limit
11 rejected (-1012) - fails closed
Controlled error counts are not something real SRAM can provide, so the captured
power-on pattern is replayed through wc_PufSetTestData() with the flips
applied - the bits are real silicon, only the extra noise is synthetic. That is
why INTERACTIVE=1 implies PUF_TEST=1.
Options 4 and 5 show what helper data is for, across a real power cycle and with
no non-volatile storage involved. 4 prints one line holding the device
identity, the helper data, and a trailing checksum over both. Copy it, power-cycle the
board, then paste it back with 5: it verifies the checksum (a mangled
paste is reported as such and changes nothing), reconstructs from freshly
re-read silicon, and compares against the identity carried in the blob, so the
board reports the result itself rather than leaving you to compare hex by eye. Nothing secret
leaves the part - the helper is public, which is why it can travel out over the
wire and back in again.
The reader ignores whitespace, needs no trailing newline, and discards its
accumulation if it sees any non-hex text, so a selection that catches the
surrounding prose still loads correctly. q aborts.
Note that a soft reset does not re-randomise SRAM. Only a real power cycle produces a fresh power-on readout.
Output
Build output is placed in ./Build/:
puf_example.elf- Loadable ELF binarypuf_example.hex- Intel HEX for flash programmers
Flashing
OpenOCD (NUCLEO-H563ZI)
openocd -f interface/stlink.cfg -f target/stm32h5x.cfg \
-c "program Build/puf_example.elf verify reset exit"
When multiple ST-Links are connected, specify the serial number:
openocd -f interface/stlink.cfg \
-c "adapter serial <YOUR_SERIAL>" \
-f target/stm32h5x.cfg \
-c "program Build/puf_example.elf verify reset exit"
UART Output
Connect to the board's UART (typically 115200 baud) to see output:
--- wolfCrypt SRAM PUF Example ---
PUF initialized.
Mode: TEST (synthetic SRAM data)
Enrollment complete.
Identity (enrollment): 3ad99904f92897bad1a21bc9cbc3ab8f2dc4bc40dfe6e161c741f98ef8dd7e01
Derived key (enrollment): aa8573f70a3253ca567500bdcd610face6a140e5fc68047e02d3f13958dcc480
--- Simulating power cycle (noisy SRAM) ---
Reconstruction complete (BCH corrected noisy bits).
Identity (reconstructed): 3ad99904f92897bad1a21bc9cbc3ab8f2dc4bc40dfe6e161c741f98ef8dd7e01
PASS: Identity matches after reconstruction.
Derived key (reconstructed): aa8573f70a3253ca567500bdcd610face6a140e5fc68047e02d3f13958dcc480
PASS: Derived key matches after reconstruction.
--- PUF example complete ---
Customizing for Your MCU
Linker Script
Edit linker.ld to match your MCU's memory map:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K /* your flash size */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 636K /* total - PUF_RAM */
PUF_RAM (rw) : ORIGIN = 0x2009F000, LENGTH = 4K /* end of SRAM */
}
The PUF_RAM region must be at the end of SRAM and marked NOLOAD so the
startup code does not zero it.
Architecture
Edit the ARCHFLAGS in Makefile:
ARCHFLAGS = -mcpu=cortex-m33 -mthumb
API Usage
wc_PufCtx ctx;
uint8_t helperData[WC_PUF_HELPER_BYTES];
uint8_t key[WC_PUF_KEY_SZ];
/* First boot: Enroll */
wc_PufInit(&ctx);
wc_PufReadSram(&ctx, sram_addr, sram_size);
wc_PufEnroll(&ctx);
memcpy(helperData, ctx.helperData, WC_PUF_HELPER_BYTES);
/* Store helperData to flash/NVM (it is NOT secret) */
/* Subsequent boots: Reconstruct */
wc_PufInit(&ctx);
wc_PufReadSram(&ctx, sram_addr, sram_size);
wc_PufReconstruct(&ctx, helperData, WC_PUF_HELPER_BYTES);
wc_PufDeriveKey(&ctx, info, infoSz, key, sizeof(key));
/* Always zeroize when done */
wc_PufZeroize(&ctx);
Security Notes
- Helper data is public - It does not reveal the key. Safe to store unencrypted in flash or transmit over the network.
- SRAM must not be accessed before PUF read - Any read or write to the
PUF SRAM region before
wc_PufReadSram()will corrupt the power-on entropy. - Production RNG - This example wires wolfCrypt's RNG through
CUSTOM_RAND_GENERATE_BLOCK(inuser_settings.h) tocustom_rand_gen_block()instm32.c, which uses the STM32H5 RNG peripheral over HSI48. When porting to another MCU, replace the implementation behindcustom_rand_gen_block()(or remapCUSTOM_RAND_GENERATE_BLOCKto your platform's hardware RNG hook).
Reproducing on the m33mu Emulator
The m33mu Cortex-M33 emulator can simulate cold-boot SRAM and seeded noise so the BCH reconstruction path can be exercised without rebooting real hardware:
# Deterministic SRAM, boot 0 - enrolls and reconstructs cleanly
m33mu --puf-seed 0xDEADBEEF --puf-cold-boot 0 Build/puf_example.elf
# Same seed/boot with 2 bit flips per 127-bit codeword - within BCH(t=10)
m33mu --puf-seed 0xDEADBEEF --puf-cold-boot 0 --puf-noise 2 \
Build/puf_example.elf
Identity must match between the enrollment and reconstruction prints as long as noise stays within the BCH correction budget (10 flips per 127-bit codeword; safe margin 2-4).