Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/examples-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,14 @@ examples:
mode: skip
reason: "shared helper (btle-sim.c) linked by btle/ecies and btle/tls; not an example"

- id: puf-host-test
path: puf/host_test
mode: skip
reason: >-
Host behavioral test harness for puf's interactive demo, not an example:
puf.yml builds it and runs driver.py against wolfSSL master (the demo
needs post-v5.9.2 PUF APIs, so the stable ref is excluded there)

- id: hsm-dtls-client
path: hsm/dtls_client
mode: skip
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/puf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ 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)
Comment thread
dgarske marked this conversation as resolved.
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
make WOLFSSL_ROOT=/tmp/wolfssl INTERACTIVE=1 SHOW_KEYS=1

# Host build of the interactive demo with a stdio HAL, driven 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.
- name: Run interactive demo behavioral test (wolfSSL master only)
if: matrix.wolfssl_ref == 'master'
run: |
set -euo pipefail
cd puf/host_test
make WOLFSSL_ROOT=/tmp/wolfssl run

- name: Assert it really cross-compiled
run: |
Expand Down
42 changes: 38 additions & 4 deletions puf/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,31 @@ 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

# The interactive demo never writes derived keys to the UART by default (the
# console is unauthenticated). SHOW_KEYS=1 opts in for lab use.
SHOW_KEYS ?= 0

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

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

BUILD_SUFFIX =
ifeq ($(INTERACTIVE),1)
BUILD_SUFFIX := $(BUILD_SUFFIX)-interactive
ifeq ($(SHOW_KEYS),1)
CFLAGS += -DPUF_DEMO_SHOW_KEYS
BUILD_SUFFIX := $(BUILD_SUFFIX)-showkeys
endif
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 +82,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 +136,5 @@ $(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-interactive-showkeys ./Build-hw
$(MAKE) -C host_test clean
91 changes: 91 additions & 0 deletions puf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,97 @@ 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/`:
Expand Down
35 changes: 35 additions & 0 deletions puf/host_test/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Host behavioral test for the interactive PUF demo: builds
# main_interactive.c with a stdio HAL (harness.c) and drives the menu with
# driver.py, covering enrollment, the noise sweep, blob dump/recovery,
# checksum and identity-mismatch rejection, abort, and the fail-closed
# unhealthy-readout path. Requires wolfSSL master (same as INTERACTIVE=1).
WOLFSSL_ROOT ?= ../../../wolfssl
CC ?= gcc
CFLAGS = -Wall -Og -g -DWOLFSSL_USER_SETTINGS -DWOLFSSL_PUF_TEST
CFLAGS += -I.. -I$(WOLFSSL_ROOT)

SRC = ../main_interactive.c harness.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/puf.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/sha256.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/kdf.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/hmac.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/hash.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/memory.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/wc_port.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/error.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/misc.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/logging.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/random.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/sp_int.c
SRC += $(WOLFSSL_ROOT)/wolfcrypt/src/sha3.c

puf_host_test: $(SRC) ../user_settings.h
$(CC) $(CFLAGS) -o $@ $(filter %.c,$^)

run: puf_host_test
python3 driver.py ./puf_host_test

clean:
rm -f puf_host_test

.PHONY: run clean
Loading
Loading