wolfssl-examples/puf/README.md

270 lines
9.5 KiB
Markdown

# 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:
1. **Enrollment** - Read raw SRAM, generate helper data using BCH(127,64,t=10)
error-correcting codes
2. **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)
3. **Key derivation** - Use HKDF-SHA256 to derive a 256-bit cryptographic key
4. **Device identity** - SHA-256 hash of stable bits serves as a unique device ID
## Building
### Requirements
- `arm-none-eabi-gcc` toolchain
### Test Mode (default)
Uses synthetic SRAM data -- runs on any target, no real hardware needed:
```bash
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:
```bash
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`.
```bash
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 binary
- `puf_example.hex` - Intel HEX for flash programmers
## Flashing
### OpenOCD (NUCLEO-H563ZI)
```bash
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:
```bash
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:
```ld
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`:
```makefile
ARCHFLAGS = -mcpu=cortex-m33 -mthumb
```
## API Usage
```c
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` (in `user_settings.h`) to
`custom_rand_gen_block()` in `stm32.c`, which uses the STM32H5 RNG
peripheral over HSI48. When porting to another MCU, replace the
implementation behind `custom_rand_gen_block()` (or remap
`CUSTOM_RAND_GENERATE_BLOCK` to 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:
```bash
# 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).