Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/puf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ jobs:
bash "$GITHUB_WORKSPACE/.github/scripts/git-clone-retry.sh" -q --depth 1 --branch '${{ matrix.wolfssl_ref }}' https://github.com/wolfSSL/wolfssl /tmp/wolfssl
cd puf
make WOLFSSL_ROOT=/tmp/wolfssl
make WOLFSSL_ROOT=/tmp/wolfssl PUF_TEST=0

# The interactive demo uses PUF APIs added after v5.9.2, so it only
# builds against master (older trees stop at a #error in the source).
# PUF_TEST=0 on the same line proves the override forces test mode on.
- name: Build puf interactive demo (wolfSSL master only)
if: matrix.wolfssl_ref == 'master'
run: |
set -euo pipefail
cd puf
make WOLFSSL_ROOT=/tmp/wolfssl INTERACTIVE=1
make WOLFSSL_ROOT=/tmp/wolfssl INTERACTIVE=1 PUF_TEST=0

- name: Assert it really cross-compiled
run: |
Expand Down
33 changes: 29 additions & 4 deletions puf/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,27 @@ NM = $(TOOLCHAIN)nm
# wolfSSL root (relative to this directory)
WOLFSSL_ROOT ?= ../../wolfssl

# Build output
BUILD_DIR = ./Build
# Build output. Each configuration gets its own directory so switching
# INTERACTIVE / PUF_TEST between invocations can never relink stale objects
# from the previous configuration (the default build stays in ./Build).
BIN = puf_example

# PUF test mode (default on): synthetic SRAM data for testing without hardware.
# Set PUF_TEST=0 to build for real hardware SRAM.
PUF_TEST ?= 1

# Set INTERACTIVE=1 to build the UART menu demo (main_interactive.c) instead of
# the one-shot example. It captures the real power-on SRAM and then replays it
# through wc_PufSetTestData so a known number of bit flips can be injected, so
# it needs the test hooks compiled in: PUF_TEST is forced on (override beats a
# contradictory PUF_TEST=0 on the command line). Requires wolfSSL master (the
# demo uses PUF APIs added after v5.9.2; the build stops with a clear #error
# on older trees).
INTERACTIVE ?= 0
ifeq ($(INTERACTIVE),1)
override PUF_TEST := 1
endif

# Architecture
ARCHFLAGS = -mcpu=cortex-m33 -mthumb -mabi=aapcs

Expand All @@ -41,6 +54,15 @@ ifeq ($(PUF_TEST),1)
CFLAGS += -DWOLFSSL_PUF_TEST
endif

BUILD_SUFFIX =
ifeq ($(INTERACTIVE),1)
BUILD_SUFFIX := $(BUILD_SUFFIX)-interactive
endif
ifeq ($(PUF_TEST),0)
BUILD_SUFFIX := $(BUILD_SUFFIX)-hw
endif
BUILD_DIR ?= ./Build$(BUILD_SUFFIX)

# Linker flags
LDFLAGS = $(ARCHFLAGS)
LDFLAGS += --specs=nosys.specs --specs=nano.specs
Expand All @@ -52,7 +74,11 @@ LDFLAGS += -T./linker.ld
LIBS = -lm

# Source files
ifeq ($(INTERACTIVE),1)
Comment thread
dgarske marked this conversation as resolved.
Comment thread
dgarske marked this conversation as resolved.
SRC_C = main_interactive.c
else
SRC_C = main.c
endif
SRC_C += startup.c
SRC_C += stm32.c

Expand Down Expand Up @@ -102,5 +128,4 @@ $(BUILD_DIR)/$(BIN).hex: $(BUILD_DIR)/$(BIN).elf
$(OBJCOPY) -O ihex $< $@

clean:
rm -f $(BUILD_DIR)/*.elf $(BUILD_DIR)/*.hex $(BUILD_DIR)/*.map
rm -f $(BUILD_DIR)/*.o $(BUILD_DIR)/*.sym $(BUILD_DIR)/*.disasm
rm -rf ./Build ./Build-interactive ./Build-hw
76 changes: 76 additions & 0 deletions puf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,82 @@ 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

```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.

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`. If the power-on readout is rejected by
the health band (which is what a warm reset looks like, since SRAM keeps the
previous image's data), the demo falls back to a synthetic pattern and says so,
so the extractor demos still run.

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/`:
Expand Down
Loading
Loading