/* 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 #include #include /* 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 #include #include 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; } } }