puf: fix transmit-only UART HAL and add an interactive INTERACTIVE=1 demo mode

pull/623/head
David Garske 2026-08-31 10:39:33 -07:00
parent d421168ad8
commit ef8181959e
9 changed files with 1024 additions and 6 deletions

View File

@ -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

View File

@ -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)
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: |

View File

@ -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
@ -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
@ -52,7 +82,11 @@ LDFLAGS += -T./linker.ld
LIBS = -lm
# Source files
ifeq ($(INTERACTIVE),1)
SRC_C = main_interactive.c
else
SRC_C = main.c
endif
SRC_C += startup.c
SRC_C += stm32.c
@ -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

View File

@ -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/`:

View File

@ -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

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Behavioral test driver for the interactive PUF demo (host build).
Drives the menu over stdin/stdout and asserts on the demo's output markers:
enrollment, the sweep gate and correction cliff, blob dump and recovery,
checksum rejection, identity-mismatch rejection, paste abort, and the
fail-closed unhealthy-readout path. Exits nonzero on the first failure.
"""
import os
import re
import select
import subprocess
import sys
import time
BIN = sys.argv[1] if len(sys.argv) > 1 else "./puf_host_test"
TIMEOUT = 15
class Demo:
def __init__(self, env=None):
e = dict(os.environ)
if env:
e.update(env)
self.p = subprocess.Popen([BIN], stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, env=e)
self.buf = b""
def send(self, text):
self.p.stdin.write(text.encode())
self.p.stdin.flush()
def expect(self, *patterns):
"""Read until every pattern has appeared (in the stream so far)."""
deadline = time.time() + TIMEOUT
remaining = list(patterns)
while remaining:
remaining = [p for p in remaining
if not re.search(p.encode(), self.buf)]
if not remaining:
break
if time.time() > deadline:
raise AssertionError(
"timeout waiting for %r; got:\n%s" %
(remaining, self.buf.decode(errors="replace")[-2000:]))
r, _, _ = select.select([self.p.stdout], [], [], 0.2)
if r:
chunk = os.read(self.p.stdout.fileno(), 65536)
if not chunk:
raise AssertionError(
"EOF waiting for %r; got:\n%s" %
(remaining, self.buf.decode(errors="replace")[-2000:]))
self.buf += chunk
def absent(self, pattern):
if re.search(pattern.encode(), self.buf):
raise AssertionError("unexpected %r in:\n%s" %
(pattern, self.buf.decode(errors="replace")))
def clear(self):
self.buf = b""
def close(self):
self.p.stdin.close()
try:
self.p.wait(timeout=TIMEOUT)
finally:
if self.p.poll() is None:
self.p.kill()
def checksum(data):
s = 0xFFFF
for b in data:
s = ((s << 5) ^ (s >> 11) ^ b) & 0xFFFF
return s
def healthy_run():
d = Demo()
d.expect(r"interactive demo", r"inside the health band")
d.absent(r"synthetic")
# sweep is gated before an enrollment from this boot
d.clear()
d.send("2")
d.expect(r"run \[1\] enroll first")
# enroll: identity shown, key NOT shown by default
d.clear()
d.send("1")
d.expect(r"enrolled from this boot", r"identity : [0-9a-f]{32}",
r"derived OK \(not shown")
d.absent(r"derived key : [0-9a-f]{32}")
# sweep: full correction cliff, never the wrong key
d.clear()
d.send("2")
d.expect(r"<= t, the limit", r"rejected \(-\d+\) - fails closed")
d.absent(r"WRONG KEY")
# two keys: derivation succeeds, no key material on the wire
d.clear()
d.send("3")
d.expect(r"two HKDF contexts", r"derived OK \(not shown")
d.absent(r"[0-9a-f]{32}\r")
# dump the recovery blob (id + helper + 2-byte checksum, one hex line)
d.clear()
d.send("4")
d.expect(r"\r\n[0-9a-f]{300,}\r\n")
blob = re.search(rb"\r\n([0-9a-f]{300,})\r\n", d.buf).group(1).decode()
raw = bytes.fromhex(blob)
assert checksum(raw[:-2]) == int.from_bytes(raw[-2:], "big"), \
"dumped blob checksum does not verify"
# paste it back: checksum OK, same key
d.clear()
d.send("5")
d.expect(r"paste the recovery blob")
d.send(blob)
d.expect(r"checksum [0-9a-f]{4} OK", r"SAME KEY")
# sweep gated again after a loaded blob
d.clear()
d.send("2")
d.expect(r"run \[1\] enroll first")
# mangled checksum: rejected, nothing changed
d.clear()
d.send("5")
d.expect(r"paste the recovery blob")
bad = blob[:-1] + ("0" if blob[-1] != "0" else "1")
d.send(bad)
d.expect(r"checksum mismatch", r"nothing was changed")
# corrupted identity with a recomputed valid checksum: MISMATCH, no commit
body = bytearray(raw[:-2])
body[0] ^= 0x01
wrong = body.hex() + format(checksum(body), "04x")
d.clear()
d.send("5")
d.expect(r"paste the recovery blob")
d.send(wrong)
d.expect(r"MISMATCH - this blob does not belong",
r"nothing was changed")
# truncated paste + q: aborted
d.clear()
d.send("5")
d.expect(r"paste the recovery blob")
d.send(blob[:40] + "q")
d.expect(r"aborted")
d.close()
print("healthy-path scenarios: PASS")
def unhealthy_run():
d = Demo(env={"PUF_HOST_UNHEALTHY": "1"})
d.expect(r"REJECTED by the health band",
r"enrollment and key derivation are disabled")
d.absent(r"synthetic")
for opt in "135":
d.clear()
d.send(opt)
# option 3 is additionally gated on enrollment; either refusal is a
# correct fail-closed response
d.expect(r"(failed the health check|run \[1\] enroll first)")
d.absent(r"[0-9a-f]{32}")
d.close()
print("unhealthy fail-closed scenarios: PASS")
def main():
healthy_run()
unhealthy_run()
print("ALL PASS")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,77 @@
/* harness.c - host-side stand-ins for the interactive PUF demo's HAL so the
* menu logic, blob parsing, and fail-closed paths can be exercised without
* hardware. The UART becomes stdio; the PUF region is seeded with a balanced
* deterministic pattern before main() runs, or left all-zero (which fails the
* Hamming-weight health band) when PUF_HOST_UNHEALTHY is set.
*
* Copyright (C) 2006-2026 wolfSSL Inc.
*
* This file is part of wolfSSL.
*
* wolfSSL is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfSSL is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <wolfssl/wolfcrypt/settings.h>
#include <wolfssl/wolfcrypt/puf.h>
extern volatile uint8_t puf_sram_region[WC_PUF_RAW_BYTES];
__attribute__((constructor))
static void seed_region(void)
{
uint32_t x = 0x12345678u;
unsigned int i;
if (getenv("PUF_HOST_UNHEALTHY") != NULL) {
return; /* all-zero: rejected by the health band */
}
for (i = 0; i < (unsigned int)WC_PUF_RAW_BYTES; i++) {
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
puf_sram_region[i] = (uint8_t)x;
}
}
void hal_init(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
}
int uart_getc(void)
{
int c = getchar();
if (c == EOF) {
exit(0);
}
return c;
}
void uart_drain(void)
{
}
int custom_rand_gen_block(unsigned char* output, unsigned int sz)
{
static uint32_t x = 0xA5A5A5A5u;
unsigned int i;
for (i = 0; i < sz; i++) {
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
output[i] = (unsigned char)x;
}
return 0;
}

View File

@ -0,0 +1,527 @@
/* main_interactive.c - interactive wolfCrypt PUF demo over UART
*
* Captures the real power-on SRAM at reset, reports whether it is a usable
* PUF source, and then offers an interactive menu over the UART:
*
* 1 enroll and show identity / derived key / helper size
* 2 noise sweep - inject a known number of bit flips per codeword and
* show where BCH stops correcting (the "correction cliff")
* 3 derive two unrelated keys from the same silicon (HKDF context)
* 4 dump the helper data, which is public
* 5 reconstruct from the stored helper and compare to enrollment
* r soft reboot
*
* The noise sweep needs controllable error counts, which real SRAM cannot
* provide, so the captured power-on pattern is replayed through
* wc_PufSetTestData with a known number of flips applied. The bits are real
* silicon; only the extra noise is synthetic.
*
* Lines are terminated with an explicit \r\n: the host behavioral test
* (host_test/driver.py) matches literal CRLF, and on the hardware UART the
* extra CR added by _write() is harmless.
*
* Copyright (C) 2006-2026 wolfSSL Inc.
*
* This file is part of wolfSSL.
*
* wolfSSL is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfSSL is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <wolfssl/wolfcrypt/settings.h>
#include <wolfssl/wolfcrypt/puf.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
/* This demo drives PUF APIs added after the v5.9.2 stable release
* (WC_PUF_RAW_STRIDE_BITS, wc_PufCheckSram, wc_PufGetParams,
* wc_PufGetProfileId, wc_PufGetHelperData), so INTERACTIVE=1 needs wolfSSL
* master. The one-shot example still builds against the stable release. */
#ifndef WC_PUF_RAW_STRIDE_BITS
#error "INTERACTIVE=1 requires wolfSSL master (post-v5.9.2 PUF API)"
#endif
#include <stdio.h>
#include <string.h>
#include <stdint.h>
extern void hal_init(void);
extern int uart_getc(void);
extern void uart_drain(void);
static unsigned int helper_sum(const uint8_t* d, uint32_t len);
static unsigned int helper_sum_cont(unsigned int sum, const uint8_t* d,
uint32_t len);
/* Raw power-on SRAM. NOLOAD section: startup must not zero it. Non-static
* so the host behavioral test harness can seed it before main() runs. */
__attribute__((section(".puf_sram")))
volatile uint8_t puf_sram_region[WC_PUF_RAW_BYTES];
/* Snapshot taken before anything else can disturb the region. */
static uint8_t g_raw[WC_PUF_RAW_BYTES];
static uint8_t g_work[WC_PUF_RAW_BYTES];
static uint8_t g_helper[WC_PUF_HELPER_BYTES];
static uint8_t g_id[WC_PUF_ID_SZ];
static int g_enrolled = 0;
/* The noise sweep injects exact flip counts against the readout the helper
* was enrolled from, so it needs an enrollment taken from THIS boot's g_raw -
* a blob loaded from a previous boot has an unknown natural flip baseline. */
static int g_freshEnroll = 0;
static int g_rawHealthy = 0;
static int g_onesPct = 0;
/* Derived keys are never written to the UART by default: the console is an
* unauthenticated physical interface. make SHOW_KEYS=1 opts in for lab use. */
static void print_key(const char* label, const uint8_t* key, uint32_t len)
{
#ifdef PUF_DEMO_SHOW_KEYS
uint32_t i;
printf("%s", label);
for (i = 0; i < 16u && i < len; i++)
printf("%02x", key[i]);
printf("\r\n");
#else
printf("%s%u bytes derived OK (not shown; build SHOW_KEYS=1 to "
"display)\r\n", label, (unsigned int)len);
(void)key;
#endif
}
static void print_hex(const char* label, const uint8_t* d, uint32_t len)
{
uint32_t i;
printf("%s", label);
for (i = 0; i < len; i++)
printf("%02x", d[i]);
printf("\r\n");
}
static int ones_percent(const uint8_t* d, uint32_t len)
{
uint32_t i;
int b, ones = 0;
for (i = 0; i < len; i++) {
for (b = 0; b < 8; b++) {
if (d[i] & (1u << b))
ones++;
}
}
return (int)((ones * 100u) / (len * 8u));
}
/* Flip 'flips' bits inside each codeword-sized stride of the pattern. */
static void add_noise(uint8_t* d, int flips)
{
int cw, f, bit;
int stride = WC_PUF_RAW_STRIDE_BITS;
for (cw = 0; cw < WC_PUF_NUM_CODEWORDS; cw++) {
for (f = 0; f < flips; f++) {
bit = cw * stride + (f * 7) + 3;
if ((bit / 8) < (int)WC_PUF_RAW_BYTES)
d[bit / 8] ^= (uint8_t)(1u << (bit % 8));
}
}
}
/* Load a pattern into a fresh context and read it in. */
static int load_ctx(wc_PufCtx* ctx, const uint8_t* pattern)
{
int ret = wc_PufInit(ctx);
if (ret != 0)
return ret;
ret = wc_PufSetTestData(ctx, pattern, WC_PUF_RAW_BYTES);
if (ret != 0)
return ret;
return wc_PufReadSram(ctx, pattern, WC_PUF_RAW_BYTES);
}
static int require_healthy(void)
{
if (!g_rawHealthy) {
printf(" the power-on readout failed the health check, so this is\r\n"
" disabled - deriving from anything else would produce a\r\n"
" device-independent key. Power-cycle the board (a warm\r\n"
" reset leaves old data in SRAM) and try again.\r\n");
return 0;
}
return 1;
}
static void do_enroll(void)
{
wc_PufCtx ctx;
uint8_t key[WC_PUF_KEY_SZ];
int ret;
if (!require_healthy())
return;
ret = load_ctx(&ctx, g_raw);
if (ret != 0) {
printf(" readout rejected: %d\r\n", ret);
wc_PufZeroize(&ctx);
return;
}
ret = wc_PufEnroll(&ctx);
if (ret != 0) {
printf(" enroll failed: %d\r\n", ret);
wc_PufZeroize(&ctx);
return;
}
ret = wc_PufGetHelperData(&ctx, g_helper, sizeof(g_helper));
if (ret == 0)
ret = wc_PufGetIdentity(&ctx, g_id, sizeof(g_id));
if (ret == 0)
ret = wc_PufDeriveKey(&ctx, (const byte*)"nv-integrity", 12,
key, sizeof(key));
if (ret != 0) {
printf(" enroll failed: %d\r\n", ret);
wc_ForceZero(key, sizeof(key));
wc_PufZeroize(&ctx);
return;
}
printf(" enrolled from this boot's power-on SRAM readout\r\n");
print_hex(" identity : ", g_id, 16);
print_key(" derived key : ", key, sizeof(key));
printf(" helper data : %d bytes, stored in the clear\r\n",
(int)sizeof(g_helper));
g_enrolled = 1;
g_freshEnroll = 1;
wc_ForceZero(key, sizeof(key));
wc_PufZeroize(&ctx);
}
static void do_sweep(void)
{
wc_PufCtx ctx;
uint8_t id[WC_PUF_ID_SZ];
int flips, ret, m, n, k, t, cw;
if (!g_freshEnroll) {
printf(" run [1] enroll first - the sweep needs a helper enrolled\r\n"
" from this boot's readout so the injected flip counts are\r\n"
" exact (a loaded blob has an unknown natural flip baseline)\r\n");
return;
}
wc_PufGetParams(&m, &n, &k, &t, &cw);
printf(" BCH(%d,%d,t=%d), %d codewords - correcting up to %d flips per "
"%d-bit codeword\r\n", n, k, t, cw, t, n);
printf(" flips/codeword result\r\n");
for (flips = 0; flips <= t + 3; flips++) {
XMEMCPY(g_work, g_raw, sizeof(g_work));
add_noise(g_work, flips);
ret = load_ctx(&ctx, g_work);
if (ret == 0)
ret = wc_PufReconstruct(&ctx, g_helper, sizeof(g_helper));
if (ret == 0)
ret = wc_PufGetIdentity(&ctx, id, sizeof(id));
printf(" %2d ", flips);
if (ret != 0) {
printf("rejected (%d) - fails closed", ret);
}
else if (XMEMCMP(id, g_id, sizeof(id)) == 0) {
printf("identity matches");
}
else {
printf("WRONG KEY - would be a bug");
}
if (flips == t)
printf(" <= t, the limit");
printf("\r\n");
wc_PufZeroize(&ctx);
}
}
static void do_two_keys(void)
{
wc_PufCtx ctx;
uint8_t k1[WC_PUF_KEY_SZ], k2[WC_PUF_KEY_SZ];
int ret;
if (!g_enrolled) {
printf(" run [1] enroll first\r\n");
return;
}
if (!require_healthy())
return;
ret = load_ctx(&ctx, g_raw);
if (ret == 0)
ret = wc_PufReconstruct(&ctx, g_helper, sizeof(g_helper));
if (ret != 0) {
printf(" need an enrollment first ([1]), rc=%d\r\n", ret);
wc_PufZeroize(&ctx);
return;
}
ret = wc_PufDeriveKey(&ctx, (const byte*)"nv-integrity", 12,
k1, sizeof(k1));
if (ret == 0)
ret = wc_PufDeriveKey(&ctx, (const byte*)"device-identity", 15,
k2, sizeof(k2));
if (ret != 0) {
printf(" key derivation failed: %d\r\n", ret);
wc_ForceZero(k1, sizeof(k1));
wc_ForceZero(k2, sizeof(k2));
wc_PufZeroize(&ctx);
return;
}
printf(" same silicon, same helper data, two HKDF contexts:\r\n");
print_key(" \"nv-integrity\" : ", k1, sizeof(k1));
print_key(" \"device-identity\" : ", k2, sizeof(k2));
printf(" unrelated keys - one PUF backs as many as you need\r\n");
wc_ForceZero(k1, sizeof(k1));
wc_ForceZero(k2, sizeof(k2));
wc_PufZeroize(&ctx);
}
static void do_dump_helper(void)
{
uint32_t i;
unsigned int sum;
if (!g_enrolled) {
printf(" run [1] enroll first\r\n");
return;
}
printf(" Public recovery blob: device identity, %d bytes of helper\r\n"
" data, then a 2-byte checksum over both. Triple-click the\r\n"
" single line below and copy it. After a power cycle, [5]\r\n"
" pastes it back and checks itself, so there is nothing to\r\n"
" write down.\r\n\r\n",
(int)WC_PUF_HELPER_BYTES);
for (i = 0; i < (uint32_t)WC_PUF_ID_SZ; i++) {
printf("%02x", g_id[i]);
}
for (i = 0; i < (uint32_t)WC_PUF_HELPER_BYTES; i++) {
printf("%02x", g_helper[i]);
}
sum = helper_sum(g_id, (uint32_t)WC_PUF_ID_SZ);
sum = helper_sum_cont(sum, g_helper, (uint32_t)WC_PUF_HELPER_BYTES);
printf("%04x\r\n\r\n", sum);
printf(" none of this is secret - it reveals nothing about the key, and\r\n"
" on another die it reconstructs nothing\r\n");
}
/* Small checksum so a mangled paste is reported as such rather than surfacing
* as a confusing reconstruct failure. The blob checksum covers the identity
* and the helper data together. */
static unsigned int helper_sum_cont(unsigned int sum, const uint8_t* d,
uint32_t len)
{
uint32_t i;
for (i = 0; i < len; i++) {
sum = ((sum << 5) ^ (sum >> 11) ^ d[i]) & 0xFFFFu;
}
return sum;
}
static unsigned int helper_sum(const uint8_t* d, uint32_t len)
{
return helper_sum_cont(0xFFFFu, d, len);
}
static int hexval(int c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
/* Read helper data back in as pasted hex and reconstruct from it. The helper
* is public, so it can be carried out of the device and back in over the wire.
* Pasting it after a power cycle shows the key rebuilt from silicon that has
* just been re-read, with nothing secret ever leaving the part. */
static uint8_t g_blob[WC_PUF_ID_SZ + WC_PUF_HELPER_BYTES + 2];
static void do_load_helper(void)
{
wc_PufCtx ctx;
uint8_t id[WC_PUF_ID_SZ];
uint8_t k1[WC_PUF_KEY_SZ], k2[WC_PUF_KEY_SZ];
unsigned int sum, expect;
int c, v, hi = -1, ret, match;
uint32_t n = 0;
if (!require_healthy())
return;
printf(" paste the recovery blob from [4]; q aborts.\r\n");
printf(" nothing is echoed while pasting.\r\n");
/* Terminator-free: a triple-click selection carries no trailing newline,
* so finish as soon as the blob is complete. Whitespace is ignored;
* anything else non-hex means the selection caught prose, so discard and
* resynchronise rather than shifting the stream by a nibble.
*
* g_blob is only a staging buffer: nothing is committed to the enrolled
* state (g_id / g_helper / g_enrolled) until the checksum verifies, the
* reconstruct succeeds, AND the identity matches. Every failure path
* leaves any previous enrollment untouched. */
while (n < (uint32_t)sizeof(g_blob)) {
c = uart_getc();
if (c == 'q' || c == 'Q' || c == 27) {
printf(" aborted\r\n");
return;
}
if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
continue;
v = hexval(c);
if (v < 0) {
n = 0;
hi = -1;
continue;
}
if (hi < 0) {
hi = v;
}
else {
g_blob[n++] = (uint8_t)((hi << 4) | v);
hi = -1;
}
}
/* Verify the trailing checksum (over identity + helper) before anything
* else, so a mangled paste is reported as exactly that. */
sum = helper_sum(g_blob, (uint32_t)WC_PUF_ID_SZ);
sum = helper_sum_cont(sum, g_blob + WC_PUF_ID_SZ,
(uint32_t)WC_PUF_HELPER_BYTES);
expect = ((unsigned int)g_blob[WC_PUF_ID_SZ + WC_PUF_HELPER_BYTES] << 8) |
(unsigned int)g_blob[WC_PUF_ID_SZ + WC_PUF_HELPER_BYTES + 1];
if (sum != expect) {
printf(" checksum mismatch (got %04x, blob says %04x) - the paste\r\n"
" was mangled; nothing was changed, copy the line again\r\n",
sum, expect);
return;
}
printf(" loaded identity + %d bytes of helper data, checksum %04x OK\r\n",
(int)WC_PUF_HELPER_BYTES, sum);
ret = load_ctx(&ctx, g_raw);
if (ret == 0)
ret = wc_PufReconstruct(&ctx, g_blob + WC_PUF_ID_SZ,
WC_PUF_HELPER_BYTES);
if (ret == 0)
ret = wc_PufGetIdentity(&ctx, id, sizeof(id));
if (ret == 0)
ret = wc_PufDeriveKey(&ctx, (const byte*)"nv-integrity", 12,
k1, sizeof(k1));
if (ret == 0)
ret = wc_PufDeriveKey(&ctx, (const byte*)"device-identity", 15,
k2, sizeof(k2));
if (ret != 0) {
printf(" reconstruct failed: %d\r\n", ret);
printf(" either the blob is from a different part, or the readout\r\n"
" drifted past the correction budget; nothing was changed\r\n");
}
else {
match = (XMEMCMP(id, g_blob, WC_PUF_ID_SZ) == 0);
print_hex(" identity now : ", id, 16);
print_hex(" identity enrolled : ", g_blob, 16);
printf("\r\n >>> %s <<<\r\n\r\n", match ?
"SAME KEY, REBUILT FROM SILICON AFTER POWER LOSS" :
"MISMATCH - this blob does not belong to this part");
if (match) {
print_key(" \"nv-integrity\" : ", k1, sizeof(k1));
print_key(" \"device-identity\" : ", k2, sizeof(k2));
/* Commit only now: verified, reconstructed, and matching. */
XMEMCPY(g_id, g_blob, WC_PUF_ID_SZ);
XMEMCPY(g_helper, g_blob + WC_PUF_ID_SZ, WC_PUF_HELPER_BYTES);
g_enrolled = 1;
/* Not enrolled from this boot's readout - the sweep stays off. */
g_freshEnroll = 0;
}
else {
printf(" nothing was changed\r\n");
}
}
wc_ForceZero(k1, sizeof(k1));
wc_ForceZero(k2, sizeof(k2));
wc_PufZeroize(&ctx);
}
static void menu(void)
{
printf("\r\n [1] enroll and show identity / key / helper\r\n");
printf(" [2] noise sweep - the correction cliff\r\n");
printf(" [3] two keys from one PUF\r\n");
printf(" [4] dump the public recovery blob (identity + helper)\r\n");
printf(" [5] paste the blob back after a power cycle, and verify\r\n");
printf(" [r] reboot (soft reset - SRAM is NOT re-randomised)\r\n");
printf(" [?] this menu\r\n");
}
int main(void)
{
int m, n, k, t, cw, c;
/* Snapshot the power-on SRAM before anything else can touch it. */
XMEMCPY(g_raw, (const void*)puf_sram_region, sizeof(g_raw));
hal_init();
c = wolfCrypt_Init();
if (c != 0) {
printf("ERROR: wolfCrypt_Init failed: %d\r\n", c);
for (;;) { }
}
g_onesPct = ones_percent(g_raw, sizeof(g_raw));
g_rawHealthy = (wc_PufCheckSram(g_raw, sizeof(g_raw), NULL) == 0);
wc_PufGetParams(&m, &n, &k, &t, &cw);
printf("\r\n=== wolfCrypt PUF - interactive demo ===\r\n");
printf(" profile : BCH(%d,%d,t=%d) over GF(2^%d), %d codewords, "
"id 0x%08lX\r\n", n, k, t, m, cw,
(unsigned long)wc_PufGetProfileId());
printf(" power-on SRAM readout: %d bytes, %d%% ones -> %s\r\n",
(int)sizeof(g_raw), g_onesPct,
g_rawHealthy ? "inside the health band" :
"REJECTED by the health band");
if (!g_rawHealthy) {
printf(" this region has no usable power-on entropy on this boot,\r\n"
" so enrollment and key derivation are disabled - deriving\r\n"
" from anything else would produce a device-independent\r\n"
" key. Power-cycle the board (a warm reset leaves old data\r\n"
" in SRAM) and try again.\r\n");
}
else {
printf(" the readout passed the SRAM health checks; only a genuine\r\n"
" power cycle establishes that it is fresh power-on entropy\r\n");
}
/* Drop any line noise latched in the receiver before prompting. */
uart_drain();
menu();
for (;;) {
printf("\r\n> ");
c = uart_getc();
printf("%c\r\n", (char)c);
switch (c) {
case '1': do_enroll(); break;
case '2': do_sweep(); break;
case '3': do_two_keys(); break;
case '4': do_dump_helper(); break;
case '5': do_load_helper(); break;
case 'r':
case 'R':
printf(" rebooting...\r\n\r\n");
/* AIRCR: VECTKEY 0x5FA | SYSRESETREQ */
*(volatile uint32_t*)0xE000ED0Cu = 0x05FA0004u;
for (;;) { }
default: menu(); break;
}
}
}

View File

@ -59,6 +59,8 @@
/* USART3 registers */
#define USART3_CR1 (*(volatile uint32_t *)(USART3_BASE + 0x00u))
#define USART3_RDR (*(volatile uint32_t *)(USART3_BASE + 0x24u))
#define USART3_ICR (*(volatile uint32_t *)(USART3_BASE + 0x20u))
#define USART3_CR2 (*(volatile uint32_t *)(USART3_BASE + 0x04u))
#define USART3_CR3 (*(volatile uint32_t *)(USART3_BASE + 0x08u))
#define USART3_BRR (*(volatile uint32_t *)(USART3_BASE + 0x0Cu))
@ -100,7 +102,19 @@ static void uart_init(void)
GPIO_OSPEEDR(GPIOD_BASE) |= (3u << 16); /* High speed for PD8 */
afr = GPIO_AFRH(GPIOD_BASE);
afr &= ~(0xFu << 0);
afr |= (7u << 0); /* AF7 = USART3 */
afr |= (7u << 0); /* AF7 = USART3 TX on PD8 */
GPIO_AFRH(GPIOD_BASE) = afr;
/* Configure PD9 (RX) as AF7 as well. Needed for the interactive menu;
* the original one-shot example was transmit-only. MODER pin 9 is bits
* [19:18]; AFRH pin 9 is bits [7:4]. */
moder = GPIO_MODER(GPIOD_BASE);
moder &= ~(3u << 18);
moder |= (2u << 18);
GPIO_MODER(GPIOD_BASE) = moder;
afr = GPIO_AFRH(GPIOD_BASE);
afr &= ~(0xFu << 4);
afr |= (7u << 4);
GPIO_AFRH(GPIOD_BASE) = afr;
/* Configure USART3 for UART_BAUD_HZ at the post-reset PCLK1 (see
@ -110,7 +124,7 @@ static void uart_init(void)
USART3_CR3 = 0;
USART3_PRESC = 0;
USART3_BRR = UART_PCLK_HZ / UART_BAUD_HZ;
USART3_CR1 = (1u << 3); /* TE */
USART3_CR1 = (1u << 3) | (1u << 2); /* TE | RE */
delay(10);
USART3_CR1 |= (1u << 0); /* UE */
delay(100);
@ -266,3 +280,28 @@ unsigned long my_time(unsigned long* timer)
*timer = t;
return t++;
}
/* Blocking single-character read, used by the interactive demo menu. */
int uart_getc(void)
{
/* ISR bit 5 = RXNE (receive register not empty), bit 3 = ORE (overrun).
* A pasted block arrives back-to-back with no flow control, so clear ORE
* (ICR bit 3) rather than let it wedge the receiver. */
for (;;) {
if ((USART3_ISR & (1u << 3)) != 0u)
USART3_ICR = (1u << 3);
if ((USART3_ISR & (1u << 5)) != 0u)
break;
}
return (int)(USART3_RDR & 0xFFu);
}
/* Discard anything latched in the receiver (line noise at reset). */
void uart_drain(void)
{
volatile uint32_t sink;
while ((USART3_ISR & (1u << 5)) != 0u) {
sink = USART3_RDR;
(void)sink;
}
}