From 367232d4e293c7a47de69af8805098f208381a40 Mon Sep 17 00:00:00 2001 From: David Garske Date: Mon, 3 Aug 2026 14:20:52 -0700 Subject: [PATCH] wolfCrypt SRAM PUF: configurable error correction and footprint --- .github/workflows/puf.yml | 19 +- CMakeLists.txt | 16 ++ README.md | 3 +- cmake/options.h.in | 10 + configure.ac | 23 ++- doc/dox_comments/header_files/puf.h | 60 +++++- scripts/puf_bch_genpoly.py | 245 +++++++++++++++++++++++++ wolfcrypt/src/puf.c | 272 ++++++++++++++++++---------- wolfcrypt/test/test.c | 136 ++++++++------ wolfssl/wolfcrypt/puf.h | 122 +++++++++++-- 10 files changed, 730 insertions(+), 176 deletions(-) create mode 100644 scripts/puf_bch_genpoly.py diff --git a/.github/workflows/puf.yml b/.github/workflows/puf.yml index 0b760728b35..5fa1492c4dd 100644 --- a/.github/workflows/puf.yml +++ b/.github/workflows/puf.yml @@ -18,18 +18,33 @@ permissions: jobs: puf_host_test: - name: PUF host test + name: PUF host test (${{ matrix.config }}) if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }} runs-on: ubuntu-24.04 timeout-minutes: 6 + strategy: + fail-fast: false + matrix: + # Cover every shipped BCH profile (t=7/10/13/15) and a non-default + # codeword count so the profile-specific genpoly, parity register and + # bit-packed paths are all built and exercised, not just t=10. + config: + - "--enable-puf --enable-puf-test" + - "--enable-puf=small --enable-puf-test" + - "--enable-puf=strong --enable-puf-test" + - "--enable-puf=strongest --enable-puf-test" + - "--enable-puf --enable-puf-test CPPFLAGS=-DWC_PUF_NUM_CODEWORDS=32" steps: - uses: actions/checkout@v5 name: Checkout wolfSSL + - name: Validate BCH generator polynomials + run: python3 scripts/puf_bch_genpoly.py > /dev/null + - name: Build and test PUF run: | ./autogen.sh - ./configure --enable-puf --enable-puf-test + ./configure ${{ matrix.config }} make ./wolfcrypt/test/testwolfcrypt diff --git a/CMakeLists.txt b/CMakeLists.txt index 8705b5b0220..0e05b6e5180 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2316,10 +2316,26 @@ add_option("WOLFSSL_PUF" "no" "yes;no") if(WOLFSSL_PUF) + # PUF BCH error-correction profile (only surfaced when PUF is enabled). + # Other t values and WC_PUF_NUM_CODEWORDS can still be set via CFLAGS. + add_option("WOLFSSL_PUF_PROFILE" + "PUF BCH profile: balanced (t=10), small (t=7), strong (t=13), strongest (t=15)" + "balanced" "balanced;small;strong;strongest") + list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_PUF" "-DWOLFSSL_PUF_SRAM" "-DHAVE_HKDF") + if(WOLFSSL_PUF_PROFILE STREQUAL "small") + list(APPEND WOLFSSL_DEFINITIONS "-DWC_PUF_BCH_T=7") + elseif(WOLFSSL_PUF_PROFILE STREQUAL "strong") + list(APPEND WOLFSSL_DEFINITIONS "-DWC_PUF_BCH_T=13") + elseif(WOLFSSL_PUF_PROFILE STREQUAL "strongest") + list(APPEND WOLFSSL_DEFINITIONS "-DWC_PUF_BCH_T=15") + elseif(NOT WOLFSSL_PUF_PROFILE STREQUAL "balanced") + message(FATAL_ERROR + "WOLFSSL_PUF_PROFILE must be balanced, small, strong, or strongest") + endif() override_cache(WOLFSSL_HKDF "yes") endif() diff --git a/README.md b/README.md index 644e3a55b13..bac6ee9bb3b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ information, visit the [wolfCrypt FIPS FAQ](https://www.wolfssl.com/license/fips or contact fips@wolfssl.com. wolfCrypt also includes support for deriving device-unique keys from hardware entropy -(`--enable-puf`). An example exists at +(`--enable-puf[=small|balanced|strong|strongest]`, selecting the BCH error-correction +strength). An example exists at [SRAM PUF](https://github.com/wolfSSL/wolfssl-examples/tree/master/puf). ## Why Choose wolfSSL? diff --git a/cmake/options.h.in b/cmake/options.h.in index 431f2545d6f..0c6de3c9d48 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -121,6 +121,16 @@ extern "C" { #cmakedefine HAVE_HASHDRBG #undef HAVE_HKDF #cmakedefine HAVE_HKDF +#undef WOLFSSL_PUF +#cmakedefine WOLFSSL_PUF +#undef WOLFSSL_PUF_SRAM +#cmakedefine WOLFSSL_PUF_SRAM +#undef WOLFSSL_PUF_TEST +#cmakedefine WOLFSSL_PUF_TEST +#undef WC_PUF_BCH_T +#cmakedefine WC_PUF_BCH_T @WC_PUF_BCH_T@ +#undef WC_PUF_NUM_CODEWORDS +#cmakedefine WC_PUF_NUM_CODEWORDS @WC_PUF_NUM_CODEWORDS@ #undef HAVE_HPKE #cmakedefine HAVE_HPKE #undef HAVE_KEYING_MATERIAL diff --git a/configure.ac b/configure.ac index a73a51f4f60..4df845c4813 100644 --- a/configure.ac +++ b/configure.ac @@ -8425,16 +8425,31 @@ fi # PUF AC_ARG_ENABLE([puf], - [AS_HELP_STRING([--enable-puf],[Enable SRAM PUF support (default: disabled)])], + [AS_HELP_STRING([--enable-puf],[Enable SRAM PUF: profiles small/balanced/strong/strongest = t 7/10/13/15 (default: disabled)])], [ ENABLED_PUF=$enableval ], [ ENABLED_PUF=no ] ) -if test "$ENABLED_PUF" = "yes" +if test "$ENABLED_PUF" != "no" then + # Map the profile keyword to a BCH error-correction strength (t). An + # empty PUF_BCH_T keeps the puf.h default of t=10. + PUF_BCH_T="" + PUF_PROFILE="balanced (t=10)" + case "$ENABLED_PUF" in + yes|balanced) PUF_BCH_T="" ; PUF_PROFILE="balanced (t=10)" ;; + small) PUF_BCH_T="7" ; PUF_PROFILE="small (t=7)" ;; + strong) PUF_BCH_T="13" ; PUF_PROFILE="strong (t=13)" ;; + strongest) PUF_BCH_T="15" ; PUF_PROFILE="strongest (t=15)" ;; + *) AC_MSG_ERROR([unknown --enable-puf value "$ENABLED_PUF"; use yes, small, balanced, strong, or strongest]) ;; + esac AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_PUF -DWOLFSSL_PUF_SRAM" + AS_IF([test "x$PUF_BCH_T" != "x"], + [AM_CFLAGS="$AM_CFLAGS -DWC_PUF_BCH_T=$PUF_BCH_T"]) AS_IF([test "$ENABLED_HKDF" != "yes"], [ENABLED_HKDF="yes"; AM_CFLAGS="$AM_CFLAGS -DHAVE_HKDF"]) + # normalize so downstream conditionals/summary see a plain yes + ENABLED_PUF="yes" fi # PUF test mode @@ -13771,7 +13786,11 @@ echo " * AutoSAR : $ENABLED_AUTOSAR" echo " * ML-KEM standalone: $ENABLED_MLKEM_STANDALONE" echo " * PQ/T hybrids: $ENABLED_PQC_HYBRIDS" echo " * Extra PQ/T hybrids: $ENABLED_EXTRA_PQC_HYBRIDS" +if test "$ENABLED_PUF" = "yes"; then +echo " * PUF: $ENABLED_PUF, profile $PUF_PROFILE" +else echo " * PUF: $ENABLED_PUF" +fi echo "" echo "---" diff --git a/doc/dox_comments/header_files/puf.h b/doc/dox_comments/header_files/puf.h index 73e5a2340bb..ec1a2651795 100644 --- a/doc/dox_comments/header_files/puf.h +++ b/doc/dox_comments/header_files/puf.h @@ -1,6 +1,15 @@ /*! \ingroup PUF + The SRAM PUF uses a configurable BCH(127,k,t) fuzzy extractor over GF(2^7) + with HKDF key derivation. WC_PUF_BCH_T selects the error-correction + strength (t=7, 10 default, 13, or 15) and WC_PUF_NUM_CODEWORDS (default 16) + trades SRAM footprint and helper-data size (WC_PUF_HELPER_BYTES) against + derived-key entropy. Enrollment and reconstruction must use identical + WC_PUF_BCH_T and WC_PUF_NUM_CODEWORDS; persist WC_PUF_PROFILE_ID (or the + values from wc_PufGetParams) with the helper data and compare on + reconstruction. + For a complete bare-metal example (tested on NUCLEO-H563ZI), see https://github.com/wolfSSL/wolfssl-examples/tree/master/puf */ @@ -32,7 +41,9 @@ int wc_PufInit(wc_PufCtx* ctx); \ingroup PUF \brief Read raw SRAM data into the PUF context. The sramAddr should - point to a NOLOAD linker section to preserve the power-on state. + point to a NOLOAD linker section to preserve the power-on state. The + required size, WC_PUF_RAW_BYTES, scales with WC_PUF_NUM_CODEWORDS + (256 bytes at the default 16 codewords). \return 0 on success \return BAD_FUNC_ARG if ctx or sramAddr is NULL @@ -45,7 +56,7 @@ int wc_PufInit(wc_PufCtx* ctx); _Example_ \code __attribute__((section(".puf_sram"))) - static volatile uint8_t puf_sram[256]; + static volatile uint8_t puf_sram[WC_PUF_RAW_BYTES]; wc_PufReadSram(&ctx, (const byte*)puf_sram, sizeof(puf_sram)); \endcode @@ -58,9 +69,10 @@ int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, word32 sramSz); /*! \ingroup PUF - \brief Perform PUF enrollment. Encodes raw SRAM using BCH(127,64,t=10) - and generates public helper data. After enrollment the context is ready - for key derivation and identity retrieval. + \brief Perform PUF enrollment. Encodes raw SRAM using the selected + BCH(127,k,t) profile (WC_PUF_BCH_T) and generates public helper data + (WC_PUF_HELPER_BYTES). After enrollment the context is ready for key + derivation and identity retrieval. \return 0 on success \return BAD_FUNC_ARG if ctx is NULL @@ -84,8 +96,9 @@ int wc_PufEnroll(wc_PufCtx* ctx); \ingroup PUF \brief Reconstruct stable PUF bits from noisy SRAM using stored helper - data. BCH error correction (t=10) corrects up to 10 bit flips per - 127-bit codeword. + data. BCH error correction corrects up to WC_PUF_BCH_T bit flips per + 127-bit codeword. The helper data and build configuration must match the + enrollment that produced them. \return 0 on success \return BAD_FUNC_ARG if ctx or helperData is NULL @@ -166,6 +179,37 @@ int wc_PufDeriveKey(wc_PufCtx* ctx, const byte* info, word32 infoSz, */ int wc_PufGetIdentity(wc_PufCtx* ctx, byte* id, word32 idSz); +/*! + \ingroup PUF + + \brief Report the compile-time PUF profile parameters: field size m, + codeword length n, message length k, error-correction capability t, and + the number of codewords. Each output pointer is optional (may be NULL), but + an all-NULL call is treated as a usage error. Enrollment and reconstruction + firmware must agree on all of these (and the hash); persist + WC_PUF_PROFILE_ID (which also encodes the hash selection) with the helper + data and compare before reconstruction to detect a build mismatch. + + \return 0 on success + \return BAD_FUNC_ARG if every output pointer is NULL + + \param m optional output for the GF field exponent (7 for GF(2^7)) + \param n optional output for the codeword length (127) + \param k optional output for the message length (per WC_PUF_BCH_T) + \param t optional output for the error-correction capability (WC_PUF_BCH_T) + \param numCodewords optional output for WC_PUF_NUM_CODEWORDS + + _Example_ + \code + int t, numCodewords; + wc_PufGetParams(NULL, NULL, NULL, &t, &numCodewords); + \endcode + + \sa wc_PufEnroll + \sa wc_PufReconstruct +*/ +int wc_PufGetParams(int* m, int* n, int* k, int* t, int* numCodewords); + /*! \ingroup PUF @@ -198,7 +242,7 @@ int wc_PufZeroize(wc_PufCtx* ctx); \param ctx pointer to wc_PufCtx \param data pointer to synthetic SRAM data - \param sz size of data (>= WC_PUF_RAW_BYTES, 256 bytes) + \param sz size of data (>= WC_PUF_RAW_BYTES) _Example_ \code diff --git a/scripts/puf_bch_genpoly.py b/scripts/puf_bch_genpoly.py new file mode 100644 index 00000000000..e08d0b1df87 --- /dev/null +++ b/scripts/puf_bch_genpoly.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# puf_bch_genpoly.py +# +# 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-1335, USA + +"""Generate and validate BCH(127, k, t) generator polynomials and +known-answer test (KAT) vectors for the wolfCrypt SRAM PUF (puf.c). + +The field is GF(2^7) built from the primitive polynomial +p(x) = x^7 + x^3 + 1 (0x89), matching the in-tree gf_exp/gf_log tables. + +Output for each shipped profile: + - bch_genpoly[] bytes in puf.c layout (64-bit big-endian, low deg bits, + leading x^deg coefficient implicit / masked off) + - a few message -> 127-bit systematic codeword KAT vectors + +The t=10 genpoly MUST reproduce the existing in-tree constant +{0x21,0xAB,0x81,0x5B,0xC7,0xEC,0x80,0x25}; the script asserts this before +emitting any other profile, so a bad field or convention fails loudly. + +This script is deterministic and takes no input. Run: + python3 scripts/puf_bch_genpoly.py +""" + +M = 7 +N = (1 << M) - 1 # 127 +PRIM = 0x89 # x^7 + x^3 + 1 + +# Profiles: t -> k (n-k = deg of generator polynomial). Only valid BCH(127) +# narrow-sense designed-distance profiles. See puf.h profile table. +PROFILES = [ + (7, 78), + (10, 64), # default; must match in-tree constant + (13, 50), + (15, 36), +] + +# Existing in-tree t=10 generator polynomial (puf.c) used as an oracle. +GENPOLY_T10 = [0x21, 0xAB, 0x81, 0x5B, 0xC7, 0xEC, 0x80, 0x25] + + +def build_gf(): + """Return (exp, log) tables for GF(2^7) with primitive poly 0x89.""" + exp = [0] * (N + 1) + log = [0] * (N + 1) + x = 1 + for i in range(N): + exp[i] = x + log[x] = i + x <<= 1 + if x & (1 << M): + x ^= PRIM + exp[N] = exp[0] # alpha^127 == alpha^0 == 1 (wrap) + return exp, log + + +EXP, LOG = build_gf() + + +def gf_mul(a, b): + if a == 0 or b == 0: + return 0 + return EXP[(LOG[a] + LOG[b]) % N] + + +def poly_mul_gf(p, q): + """Multiply two polynomials with GF(2^7) coefficients (lists, index = + power of x, element 0 = constant term).""" + r = [0] * (len(p) + len(q) - 1) + for i, pi in enumerate(p): + if pi == 0: + continue + for j, qj in enumerate(q): + if qj == 0: + continue + r[i + j] ^= gf_mul(pi, qj) + return r + + +def cyclotomic_coset(i): + """The 2-cyclotomic coset of i mod 127.""" + coset = set() + v = i % N + while v not in coset: + coset.add(v) + v = (v * 2) % N + return coset + + +def minimal_poly(i): + """Minimal polynomial of alpha^i: product over its coset of (x + alpha^j). + Result has binary coefficients (each 0x00 or 0x01 as a GF element).""" + m = [1] # start with 1 + for j in cyclotomic_coset(i): + m = poly_mul_gf(m, [EXP[j], 1]) # (alpha^j + x) + # coefficients must be 0 or 1 (binary); verify + for c in m: + if c not in (0, 1): + raise ValueError("minimal poly has non-binary coefficient") + return m + + +def gen_poly(t): + """Narrow-sense BCH generator polynomial = lcm of minimal polynomials of + alpha^1 .. alpha^(2t). Returned as a binary-coefficient list (index = + power of x).""" + used = set() + g = [1] + for i in range(1, 2 * t + 1): + coset = frozenset(cyclotomic_coset(i)) + if coset in used: + continue + used.add(coset) + g = poly_mul_gf(g, minimal_poly(i)) + return g + + +def genpoly_bytes(g): + """Pack g(x) into puf.c's 8..16 byte layout: a big-endian integer whose + bit i is coefficient g_i, with the leading coefficient g_deg masked off + (implicit). Byte count = ceil(deg / 8).""" + deg = len(g) - 1 + assert g[deg] == 1, "generator must be monic" + val = 0 + for i in range(deg): # exclude leading coeff + if g[i]: + val |= (1 << i) + nbytes = (deg + 7) // 8 + # 64-bit style big-endian pack sized to nbytes (matches puf.c: byte 0 is + # the most significant, top bit of byte 0 is the implicit/masked slot). + out = [] + for b in range(nbytes): + shift = 8 * (nbytes - 1 - b) + out.append((val >> shift) & 0xFF) + return deg, out + + +def bch_encode_ref(msg_bits, g): + """Reference systematic encoder matching puf.c bch_encode(): codeword = + [msg(k) | parity(deg)] MSB-first. msg_bits is a list of k bits (MSB-first + message bit order). Returns list of n bits (MSB-first).""" + deg = len(g) - 1 + k = len(msg_bits) + reg = [0] * deg # remainder register, reg[0]=MSB + for bit in msg_bits: + fb = bit ^ reg[0] + reg = reg[1:] + [0] # shift left + if fb: + # XOR non-leading generator coefficients g_{deg-1..0} into reg + for p in range(deg): + # reg position p holds coefficient of x^(deg-1-p) + if g[deg - 1 - p]: + reg[p] ^= 1 + return list(msg_bits) + reg # [msg | parity], n bits + + +def syndromes_zero(cw_bits, t): + """Return True iff every narrow-sense syndrome S_1..S_2t of the codeword is + zero, i.e. the bit list is a genuine BCH codeword. Mirrors puf.c's + bch_syndrome_eval: bit j (MSB-first) is the coefficient of x^(n-1-j), so + S_root = c(alpha^root) = XOR of alpha^(root*(n-1-j)) over the set bits.""" + n = len(cw_bits) + for root in range(1, 2 * t + 1): + s = 0 + for j, b in enumerate(cw_bits): + if b: + s ^= EXP[(root * (n - 1 - j)) % N] + if s != 0: + return False + return True + + +def bits_to_hex(bits): + """Pack an MSB-first bit list into bytes, byte-rounded.""" + out = bytearray((len(bits) + 7) // 8) + for i, b in enumerate(bits): + if b: + out[i // 8] |= 1 << (7 - (i % 8)) + return list(out) + + +def fmt_bytes(bs): + return "{ " + ", ".join("0x%02X" % b for b in bs) + " }" + + +def main(): + # Sanity: exp table spot checks against known values. + assert EXP[1] == 0x02 and EXP[7] == 0x09, "GF(2^7) table mismatch" + + print("/* Generated by scripts/puf_bch_genpoly.py - do not hand-edit */") + print("/* Field GF(2^7), primitive poly 0x89 (x^7 + x^3 + 1) */\n") + + for t, k in PROFILES: + g = gen_poly(t) + deg, bs = genpoly_bytes(g) + if deg != N - k: + raise SystemExit( + "t=%d: computed deg %d != n-k %d" % (t, deg, N - k)) + if t == 10 and bs != GENPOLY_T10: + raise SystemExit( + "t=10 genpoly %s != oracle %s (convention mismatch)" + % (fmt_bytes(bs), fmt_bytes(GENPOLY_T10))) + + print("/* t=%d k=%d deg=%d (%d parity bytes) */" % + (t, k, deg, len(bs))) + print("static const byte bch_genpoly_t%d[%d] =" % (t, len(bs))) + print(" %s;" % fmt_bytes(bs)) + + # KAT vectors: deterministic messages -> systematic codewords. + for seed in (0, 1, 2): + msg_bits = [((seed * 131 + i * 37) >> (i % 5)) & 1 + for i in range(k)] + cw = bch_encode_ref(msg_bits, g) + # self-check: the reference encoder must produce a genuine BCH + # codeword (all 2t syndromes zero), independently confirming the + # generator polynomial and the encoder algebra for this profile. + if not syndromes_zero(cw, t): + raise SystemExit( + "t=%d: KAT codeword has nonzero syndrome" % t) + print("/* KAT msg=%s" % fmt_bytes(bits_to_hex(msg_bits))) + print(" cw =%s */" % fmt_bytes(bits_to_hex(cw))) + print() + + print("/* t=10 reproduced in-tree constant: OK */") + + +if __name__ == "__main__": + main() diff --git a/wolfcrypt/src/puf.c b/wolfcrypt/src/puf.c index e645aea9521..be8a343306a 100644 --- a/wolfcrypt/src/puf.c +++ b/wolfcrypt/src/puf.c @@ -133,6 +133,42 @@ static WC_INLINE byte gf_inv(byte a) return gf_exp[GF_MASK - gf_log[a]]; } +/* ---- Bit helpers (MSB-first bit ordering within a byte array) ---- */ + +/* Get a single bit from byte array (MSB-first bit ordering) */ +static WC_INLINE byte getBit(const byte* data, int bitPos) +{ + return (data[bitPos / 8] >> (7 - (bitPos % 8))) & 1; +} + +/* Set a single bit in byte array (MSB-first bit ordering) */ +static WC_INLINE void setBit(byte* data, int bitPos, byte val) +{ + int byteIdx = bitPos / 8; + int bitIdx = 7 - (bitPos % 8); + if (val) + data[byteIdx] |= (byte)(1 << bitIdx); + else + data[byteIdx] &= (byte)~(1 << bitIdx); +} + +/* Copy nbits (MSB-first) from the front of src into dst. Copies the whole-byte + * prefix with a single memcpy and bit-loops only the < 8 trailing bits, so a + * byte-aligned length (e.g. t=10 k=64) is byte-for-byte a plain memcpy. */ +static void copyBits(byte* dst, const byte* src, int nbits) +{ + int full = nbits / 8; + int i; + + if (full > 0) + XMEMCPY(dst, src, (word32)full); + if ((nbits & 7) != 0) { + dst[full] = 0; + for (i = full * 8; i < nbits; i++) + setBit(dst, i, getBit(src, i)); + } +} + /* ---- BCH syndrome computation ---- */ /* Evaluate syndrome: S_root = c(alpha^root) where codeword bits are packed @@ -266,92 +302,116 @@ static int bch_chien_search(const byte* sigma, int deg, int* errPos) return count; } -/* ---- BCH encode: compute parity for 64-bit message ---- */ +/* ---- BCH encode: compute parity for a k-bit message ---- */ -/* Generator polynomial for BCH(127,64,t=10) over GF(2). - * This is the product of minimal polynomials of alpha^1..alpha^(2t). - * Degree = n - k = 63. Stored as 64-bit value (coefficients mod 2). - * g(x) = GCD of min polys of consecutive roots. Precomputed. */ +/* Generator polynomial for the selected BCH(127,k,t) profile over GF(2). + * g(x) is the product (lcm) of the minimal polynomials of alpha^1..alpha^(2t); + * its degree is n - k. The bytes below are generated and validated by + * scripts/puf_bch_genpoly.py (which reproduces the t=10 constant exactly as + * an oracle before emitting the others). Do NOT hand-edit: a single wrong + * bit silently corrupts every derived key. + * + * Storage layout: a big-endian integer of WC_PUF_PARITY_BYTES bytes whose + * bit i (LSB = 0) is coefficient g_i for i in [0, deg). The leading + * coefficient g_deg (== 1) is implicit; any bits above deg are zero. */ +#if WC_PUF_BCH_T == 7 + static const byte bch_genpoly[WC_PUF_PARITY_BYTES] = { + 0x00, 0xC9, 0x80, 0x11, 0xD8, 0xB0, 0x4D + }; + #define BCH_GENPOLY_DEG 49 +#elif WC_PUF_BCH_T == 10 + static const byte bch_genpoly[WC_PUF_PARITY_BYTES] = { + 0x21, 0xAB, 0x81, 0x5B, 0xC7, 0xEC, 0x80, 0x25 + }; + #define BCH_GENPOLY_DEG 63 +#elif WC_PUF_BCH_T == 13 + static const byte bch_genpoly[WC_PUF_PARITY_BYTES] = { + 0x0C, 0x93, 0x52, 0xAA, 0x6C, 0xC0, 0x54, 0x46, 0x83, 0x11 + }; + #define BCH_GENPOLY_DEG 77 +#elif WC_PUF_BCH_T == 15 + static const byte bch_genpoly[WC_PUF_PARITY_BYTES] = { + 0x04, 0xCC, 0x3C, 0xDB, 0x54, 0x87, 0xA2, 0x4F, 0xA5, 0xF3, 0xA3, 0xDD + }; + #define BCH_GENPOLY_DEG 91 +#else + #error "No generator polynomial for the selected WC_PUF_BCH_T" +#endif -/* We store g(x) as 8 bytes, MSB first, degree-63 coefficient in bit 63. - * The leading coefficient (x^63) is implicit. */ -static const byte bch_genpoly[8] = { - 0x21, 0xAB, 0x81, 0x5B, 0xC7, 0xEC, 0x80, 0x25 -}; +/* Cross-check the shipped polynomial degree against the derived n - k. */ +#if BCH_GENPOLY_DEG != WC_PUF_BCH_DEG + #error "bch_genpoly degree does not match WC_PUF_BCH_DEG (n - k)" +#endif + +/* Mask keeping only the used bits of the most-significant parity byte + * (the register holds exactly WC_PUF_BCH_DEG bits; higher bits are unused). */ +#define BCH_REG_MSB_MASK \ + ((byte)((1 << (((WC_PUF_BCH_DEG - 1) & 7) + 1)) - 1)) + +/* Read big-endian bit b (LSB = 0) from a parity-register byte array. */ +static WC_INLINE byte regGetBit(const byte* reg, int b) +{ + return (reg[WC_PUF_PARITY_BYTES - 1 - (b / 8)] >> (b % 8)) & 1; +} -/* Encode 64-bit message into 127-bit codeword. - * msg: 8 bytes (64 bits), output: 16 bytes (127 bits, MSB aligned). - * Systematic encoding: codeword = [msg(64) | parity(63)]. */ +/* Encode a k-bit message into a 127-bit codeword. + * msg: WC_PUF_MSG_BYTES bytes (k bits, MSB-first), output: WC_PUF_CW_BYTES + * bytes (127 bits, MSB aligned). Systematic: codeword = [msg(k) | parity(deg)]. + * + * The parity register is a big-endian integer of WC_PUF_PARITY_BYTES bytes + * holding WC_PUF_BCH_DEG bits; this generalizes the fixed degree-63 LFSR and + * reproduces the t=10 output byte-for-byte. */ static void bch_encode(const byte* msg, byte* codeword) { - byte shift_reg[8]; /* 63-bit shift register for parity */ + byte shift_reg[WC_PUF_PARITY_BYTES]; int i, j; XMEMSET(shift_reg, 0, sizeof(shift_reg)); - /* Process each of the 64 message bits */ + /* Process each of the k message bits (MSB-first) */ for (i = 0; i < WC_PUF_BCH_K; i++) { - int byteIdx = i / 8; - int bitIdx = 7 - (i % 8); - byte msgBit = (msg[byteIdx] >> bitIdx) & 1; + byte msgBit = getBit(msg, i); - /* feedback = msgBit XOR MSB of shift register */ - byte fb = msgBit ^ ((shift_reg[0] >> 6) & 1); + /* feedback = msgBit XOR MSB of shift register (bit deg-1) */ + byte fb = msgBit ^ regGetBit(shift_reg, WC_PUF_BCH_DEG - 1); - /* shift register left by 1 */ - for (j = 0; j < 7; j++) { + /* shift register left by 1 (big-endian, byte 0 most significant) */ + for (j = 0; j < WC_PUF_PARITY_BYTES - 1; j++) { shift_reg[j] = (byte)((shift_reg[j] << 1) | (shift_reg[j + 1] >> 7)); } - shift_reg[7] = (byte)(shift_reg[7] << 1); - /* keep the register at exactly 63 bits - bit 7 of byte 0 is unused */ - shift_reg[0] &= 0x7F; + shift_reg[WC_PUF_PARITY_BYTES - 1] = + (byte)(shift_reg[WC_PUF_PARITY_BYTES - 1] << 1); + /* keep the register at exactly WC_PUF_BCH_DEG bits */ + shift_reg[0] &= BCH_REG_MSB_MASK; /* XOR with generator if feedback is 1 */ if (fb) { - for (j = 0; j < 8; j++) { + for (j = 0; j < WC_PUF_PARITY_BYTES; j++) { shift_reg[j] ^= bch_genpoly[j]; } - /* generator polynomial bit 7 is 0; mask defensively in case it - * ever changes so the unused slot can never affect parity */ - shift_reg[0] &= 0x7F; + /* unused high bits of g are zero; mask defensively */ + shift_reg[0] &= BCH_REG_MSB_MASK; } } - /* Build codeword: [msg(64 bits) | parity(63 bits)] = 127 bits */ - XMEMSET(codeword, 0, 16); - XMEMCPY(codeword, msg, 8); /* message in first 64 bits */ - - /* parity: bits 64..126 from shift_reg bits 0..62 */ - /* shift_reg holds 63 bits in bits [6..0] of byte 0, then bytes 1..7 */ - /* We need to place these starting at bit position 64 in codeword */ - for (i = 0; i < 63; i++) { - int srcByte; - int srcBit; - - /* shift_reg MSB is bit 6 of byte 0 */ - if (i < 7) { - srcByte = 0; - srcBit = 6 - i; - } - else { - srcByte = (i - 7) / 8 + 1; - srcBit = 7 - ((i - 7) % 8); - } + /* Build codeword: [msg(k bits) | parity(deg bits)] = 127 bits */ + XMEMSET(codeword, 0, WC_PUF_CW_BYTES); + for (i = 0; i < WC_PUF_BCH_K; i++) { + setBit(codeword, i, getBit(msg, i)); + } - if (shift_reg[srcByte] & (1 << srcBit)) { - int dstPos = 64 + i; - int dstByte = dstPos / 8; - int dstBit = 7 - (dstPos % 8); - codeword[dstByte] |= (byte)(1 << dstBit); - } + /* parity: register MSB (bit deg-1) first, into codeword positions k..n-1 */ + for (i = 0; i < WC_PUF_BCH_DEG; i++) { + setBit(codeword, WC_PUF_BCH_K + i, + regGetBit(shift_reg, WC_PUF_BCH_DEG - 1 - i)); } } /* ---- BCH decode ---- */ -/* Decode 127-bit codeword, correct up to t=10 errors. - * Extracts 64-bit message into msg (8 bytes). +/* Decode 127-bit codeword, correct up to WC_PUF_BCH_T errors. + * Extracts the k-bit message into msg (WC_PUF_MSG_BYTES bytes). * Returns 0 on success, negative on uncorrectable error. */ static int bch_decode(byte* codeword, byte* msg) { @@ -374,7 +434,7 @@ static int bch_decode(byte* codeword, byte* msg) if (allZero) { /* no errors, extract message directly */ - XMEMCPY(msg, codeword, 8); + copyBits(msg, codeword, WC_PUF_BCH_K); return 0; } @@ -406,8 +466,8 @@ static int bch_decode(byte* codeword, byte* msg) return PUF_RECONSTRUCT_E; } - /* extract message (first 64 bits) */ - XMEMCPY(msg, codeword, 8); + /* extract message (first k bits) */ + copyBits(msg, codeword, WC_PUF_BCH_K); return 0; } @@ -415,28 +475,11 @@ static int bch_decode(byte* codeword, byte* msg) /* PUF API */ /* ========================================================================== */ -/* Get a single bit from byte array (MSB-first bit ordering) */ -static WC_INLINE byte getBit(const byte* data, int bitPos) -{ - return (data[bitPos / 8] >> (7 - (bitPos % 8))) & 1; -} - -/* Set a single bit in byte array (MSB-first bit ordering) */ -static WC_INLINE void setBit(byte* data, int bitPos, byte val) -{ - int byteIdx = bitPos / 8; - int bitIdx = 7 - (bitPos % 8); - if (val) - data[byteIdx] |= (byte)(1 << bitIdx); - else - data[byteIdx] &= (byte)~(1 << bitIdx); -} - /* Extract 127 bits from raw SRAM starting at given bit offset */ static void extractCodeword(const byte* sram, int bitOffset, byte* cw) { int i; - XMEMSET(cw, 0, 16); + XMEMSET(cw, 0, WC_PUF_CW_BYTES); for (i = 0; i < WC_PUF_BCH_N; i++) { setBit(cw, i, getBit(sram, bitOffset + i)); } @@ -489,10 +532,10 @@ int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, word32 sramSz) int wc_PufEnroll(wc_PufCtx* ctx) { int i, ret; - byte msg[8]; /* 64-bit message */ - byte cw[16]; /* 127-bit codeword */ - byte rawCw[16]; - byte helperCw[16]; + byte msg[WC_PUF_MSG_BYTES]; /* k-bit message */ + byte cw[WC_PUF_CW_BYTES]; /* 127-bit codeword */ + byte rawCw[WC_PUF_CW_BYTES]; + byte helperCw[WC_PUF_CW_BYTES]; WOLFSSL_ENTER("wc_PufEnroll"); @@ -519,30 +562,32 @@ int wc_PufEnroll(wc_PufCtx* ctx) XMEMSET(ctx->stableBits, 0, WC_PUF_STABLE_BYTES); for (i = 0; i < WC_PUF_NUM_CODEWORDS; i++) { - /* extract 64 message bits from raw SRAM */ - int bitOff = i * 128; /* 128-bit stride for alignment */ + /* extract k message bits from raw SRAM */ + int bitOff = i * 128; /* 128-bit stride per codeword (n=127 fits) */ int j; XMEMSET(msg, 0, sizeof(msg)); for (j = 0; j < WC_PUF_BCH_K; j++) { setBit(msg, j, getBit(ctx->rawSram, bitOff + j)); } - /* save stable bits */ - XMEMCPY(ctx->stableBits + i * 8, msg, 8); + /* save stable bits: k bits per codeword, bit-packed */ + for (j = 0; j < WC_PUF_BCH_K; j++) { + setBit(ctx->stableBits, i * WC_PUF_BCH_K + j, getBit(msg, j)); + } /* encode message into BCH codeword */ bch_encode(msg, cw); /* helper = raw XOR codeword (mask) */ extractCodeword(ctx->rawSram, bitOff, rawCw); - XMEMSET(helperCw, 0, 16); - for (j = 0; j < 16; j++) { + XMEMSET(helperCw, 0, WC_PUF_CW_BYTES); + for (j = 0; j < WC_PUF_CW_BYTES; j++) { helperCw[j] = rawCw[j] ^ cw[j]; } storeCodeword(ctx->helperData, i * WC_PUF_BCH_N, helperCw); } - /* compute identity = SHA-256(stableBits) */ + /* compute identity = hash(stableBits) */ ret = wc_PufHashDirect(ctx->stableBits, WC_PUF_STABLE_BYTES, ctx->identity); /* zeroize sensitive stack buffers */ @@ -567,10 +612,10 @@ int wc_PufEnroll(wc_PufCtx* ctx) int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz) { int i, ret; - byte rawCw[16]; - byte helperCw[16]; - byte noisyCw[16]; - byte msg[8]; + byte rawCw[WC_PUF_CW_BYTES]; + byte helperCw[WC_PUF_CW_BYTES]; + byte noisyCw[WC_PUF_CW_BYTES]; + byte msg[WC_PUF_MSG_BYTES]; WOLFSSL_ENTER("wc_PufReconstruct"); @@ -605,13 +650,13 @@ int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz) extractCodeword(ctx->rawSram, bitOff, rawCw); /* get helper data for this codeword */ - XMEMSET(helperCw, 0, 16); + XMEMSET(helperCw, 0, WC_PUF_CW_BYTES); for (j = 0; j < WC_PUF_BCH_N; j++) { setBit(helperCw, j, getBit(helperData, i * WC_PUF_BCH_N + j)); } /* noisy codeword = raw XOR helper */ - for (j = 0; j < 16; j++) { + for (j = 0; j < WC_PUF_CW_BYTES; j++) { noisyCw[j] = rawCw[j] ^ helperCw[j]; } @@ -633,7 +678,10 @@ int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz) return PUF_RECONSTRUCT_E; } - XMEMCPY(ctx->stableBits + i * 8, msg, 8); + /* store k stable bits per codeword, bit-packed */ + for (j = 0; j < WC_PUF_BCH_K; j++) { + setBit(ctx->stableBits, i * WC_PUF_BCH_K + j, getBit(msg, j)); + } } /* compute identity */ @@ -712,6 +760,34 @@ int wc_PufGetIdentity(wc_PufCtx* ctx, byte* id, word32 idSz) return 0; } +/* Report the compile-time PUF profile parameters. Each output pointer is + * optional (may be NULL). Enrollment and reconstruction firmware must agree + * on all of these; persist them (or WC_PUF_PROFILE_ID) with the helper data + * and compare before reconstruction to detect a build mismatch. */ +int wc_PufGetParams(int* m, int* n, int* k, int* t, int* numCodewords) +{ + WOLFSSL_ENTER("wc_PufGetParams"); + + /* every output is optional, but an all-NULL call is a programming error; + * reject it to match the argument-validation convention of the module */ + if (m == NULL && n == NULL && k == NULL && t == NULL && + numCodewords == NULL) + return BAD_FUNC_ARG; + + if (m != NULL) + *m = WC_PUF_BCH_M; + if (n != NULL) + *n = WC_PUF_BCH_N; + if (k != NULL) + *k = WC_PUF_BCH_K; + if (t != NULL) + *t = WC_PUF_BCH_T; + if (numCodewords != NULL) + *numCodewords = WC_PUF_NUM_CODEWORDS; + + return 0; +} + int wc_PufZeroize(wc_PufCtx* ctx) { WOLFSSL_ENTER("wc_PufZeroize"); @@ -741,4 +817,8 @@ int wc_PufSetTestData(wc_PufCtx* ctx, const byte* data, word32 sz) } #endif /* WOLFSSL_PUF_TEST */ +/* implementation-private macros - not part of the public WC_PUF_ namespace */ +#undef BCH_GENPOLY_DEG +#undef BCH_REG_MSB_MASK + #endif /* WOLFSSL_PUF */ diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 0e3679615b4..59752e69a8a 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -23963,6 +23963,34 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t sm4_test(void) #endif #ifdef WOLFSSL_PUF + +#if defined(WOLFSSL_PUF_TEST) && defined(HAVE_HKDF) && \ + (!defined(NO_SHA256) || defined(WOLFSSL_SHA3)) +/* Deterministic, profile-agnostic fill of the raw SRAM image. */ +static void puf_fill_sram(byte* sram, word32 sz) +{ + word32 i; + for (i = 0; i < sz; i++) { + sram[i] = (byte)((i * 167u + 13u) ^ (i << 3)); + } +} + +/* Flip 'count' distinct bits inside one 128-bit codeword block of the raw + * SRAM image, staying within the used n=127 bits (bit 127 is unused). Bits + * are spread 7 apart so up to t+1 flips land in distinct positions < 127. */ +static void puf_flip_bits(byte* sram, int block, int count) +{ + int i; + int base = block * 128; /* bit offset of the block */ + for (i = 0; i < count; i++) { + int bitPos = base + (i * 7); + int byteIdx = bitPos / 8; + int bitIdx = 7 - (bitPos % 8); + sram[byteIdx] ^= (byte)(1 << bitIdx); + } +} +#endif + WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) { #if defined(WOLFSSL_PUF_TEST) && defined(HAVE_HKDF) && \ @@ -23973,50 +24001,24 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) byte key2[WC_PUF_KEY_SZ]; byte id1[WC_PUF_ID_SZ]; byte id2[WC_PUF_ID_SZ]; + int block, nblocks; - /* deterministic test SRAM pattern: 256 bytes */ - WOLFSSL_SMALL_STACK_STATIC const byte testSram[WC_PUF_RAW_BYTES] = { - 0xA5, 0x3C, 0x7E, 0x19, 0xF0, 0x82, 0x4D, 0xBB, - 0x6A, 0xC1, 0x55, 0x93, 0xE7, 0x2F, 0xD8, 0x04, - 0x91, 0x68, 0xAE, 0x3B, 0xFC, 0xD7, 0x42, 0x0E, - 0x85, 0x5A, 0xC9, 0x76, 0x1D, 0xB3, 0xEF, 0x60, - 0x4C, 0x87, 0xDA, 0x25, 0xF1, 0x6E, 0x09, 0xB2, - 0x73, 0xAC, 0x58, 0xE4, 0x3F, 0x96, 0xCB, 0x17, - 0x8D, 0x62, 0xA0, 0x4E, 0xFB, 0xD5, 0x31, 0x79, - 0xC6, 0x14, 0xBE, 0x8A, 0x47, 0xF3, 0x2D, 0x98, - 0x5B, 0xE6, 0x0C, 0xA7, 0x64, 0xDF, 0x39, 0x80, - 0xB5, 0x52, 0xCD, 0x18, 0x7B, 0xE1, 0x46, 0x9F, - 0x23, 0xAA, 0x6D, 0xD0, 0x84, 0xF7, 0x3E, 0xB9, - 0x51, 0xC2, 0x0F, 0x75, 0xEC, 0x48, 0x97, 0x2A, - 0xDE, 0x63, 0xBC, 0x10, 0x86, 0xF9, 0x43, 0xAD, - 0x5E, 0xC8, 0x27, 0x94, 0x6B, 0xD1, 0x3A, 0xB0, - 0x7C, 0xE5, 0x08, 0xA1, 0x56, 0xCF, 0x4A, 0x8E, - 0x35, 0xFD, 0x61, 0xB7, 0x22, 0x99, 0xD4, 0x1C, - 0x70, 0xEE, 0x4B, 0x83, 0x2E, 0xA6, 0x5D, 0xF4, - 0x36, 0xBD, 0x69, 0xC0, 0x15, 0x9B, 0xE8, 0x41, - 0x8C, 0x53, 0xAB, 0x07, 0x74, 0xDC, 0x28, 0x95, - 0x6F, 0xD3, 0x3D, 0xBA, 0x50, 0xC4, 0x1E, 0x89, - 0xF6, 0x44, 0xAE, 0x5F, 0xC7, 0x12, 0x9A, 0xE3, - 0x37, 0xB1, 0x66, 0xDB, 0x29, 0x8B, 0x54, 0xA2, - 0x0D, 0x78, 0xED, 0x40, 0x93, 0x2C, 0xBF, 0x67, - 0xD6, 0x3C, 0xA9, 0x57, 0xCE, 0x1A, 0x81, 0xF5, - 0x49, 0x9E, 0x24, 0xB8, 0x6C, 0xD2, 0x38, 0xA4, - 0x5C, 0xE9, 0x01, 0x7A, 0xDD, 0x45, 0x90, 0x2B, - 0xBB, 0x62, 0xC3, 0x16, 0x8F, 0xF8, 0x4E, 0xA3, - 0x34, 0xB6, 0x6E, 0xD9, 0x20, 0x9C, 0x59, 0xE2, - 0x0B, 0x77, 0xEA, 0x42, 0x8D, 0x33, 0xCA, 0x5B, - 0xFE, 0x11, 0x7F, 0xA8, 0x46, 0xD4, 0x2F, 0x96, - 0x65, 0xBC, 0x03, 0x9D, 0xE0, 0x58, 0xAF, 0x71, - 0xC5, 0x1B, 0x87, 0xFA, 0x4D, 0xB4, 0x26, 0xDF - }; - + /* deterministic test SRAM, sized to the selected profile. These buffers + * scale with WC_PUF_NUM_CODEWORDS, so keep them off the stack under + * WOLFSSL_SMALL_STACK (static there, plain stack otherwise). */ + WOLFSSL_SMALL_STACK_STATIC byte testSram[WC_PUF_RAW_BYTES]; /* noisy SRAM: same as testSram but with a few flipped bits */ - byte noisySram[WC_PUF_RAW_BYTES]; - byte helperBuf[WC_PUF_HELPER_BYTES]; + WOLFSSL_SMALL_STACK_STATIC byte noisySram[WC_PUF_RAW_BYTES]; + WOLFSSL_SMALL_STACK_STATIC byte helperBuf[WC_PUF_HELPER_BYTES]; const byte info[] = "puf-test-context"; WOLFSSL_ENTER("puf_test"); + puf_fill_sram(testSram, (word32)sizeof(testSram)); + + /* number of blocks we exercise with noise (cap at 3) */ + nblocks = WC_PUF_NUM_CODEWORDS < 3 ? WC_PUF_NUM_CODEWORDS : 3; + /* ---- Test 1: Init ---- */ ret = wc_PufInit(&ctx); if (ret != 0) @@ -24077,14 +24079,12 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) if (XMEMCMP(key1, key2, WC_PUF_KEY_SZ) != 0) return WC_TEST_RET_ENC_NC; - /* ---- Test 4: Reconstruct with noisy data (few bit flips) ---- */ + /* ---- Test 4: Reconstruct with noisy data at the correction limit ---- */ XMEMCPY(noisySram, testSram, sizeof(testSram)); - /* flip a few bits in each 128-bit block (within BCH correction limit) */ - noisySram[0] ^= 0x01; /* block 0: 1 bit flip */ - noisySram[16] ^= 0x03; /* block 1: 2 bit flips */ - noisySram[32] ^= 0x05; /* block 2: 2 bit flips */ - noisySram[48] ^= 0x11; /* block 3: 2 bit flips */ - noisySram[64] ^= 0x80; /* block 4: 1 bit flip */ + /* flip exactly WC_PUF_BCH_T bits per block (max the decoder must fix) */ + for (block = 0; block < nblocks; block++) { + puf_flip_bits(noisySram, block, WC_PUF_BCH_T); + } ret = wc_PufInit(&ctx); if (ret != 0) @@ -24118,13 +24118,16 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) if (XMEMCMP(key1, key2, WC_PUF_KEY_SZ) != 0) return WC_TEST_RET_ENC_NC; - /* ---- Test 4b: Reconstruct with too many bit errors (should fail) ---- */ + /* ---- Test 4b: Too many bit errors (exceeds correction limit) ---- * + * Flipping t+1 bits must never silently reproduce the CORRECT key: the + * decoder either fails, or (if a t+1 pattern lands near another valid + * codeword) yields a different identity. Both are acceptable; a matching + * identity is not. */ { - byte tooNoisySram[WC_PUF_RAW_BYTES]; + WOLFSSL_SMALL_STACK_STATIC byte tooNoisySram[WC_PUF_RAW_BYTES]; XMEMCPY(tooNoisySram, testSram, sizeof(testSram)); - /* flip 12 bits in block 0 (exceeds t=10 correction limit) */ - tooNoisySram[0] ^= 0xFF; /* 8 flips */ - tooNoisySram[1] ^= 0x0F; /* 4 flips = 12 total > t=10 */ + /* flip WC_PUF_BCH_T + 1 bits in block 0 (exceeds correction limit) */ + puf_flip_bits(tooNoisySram, 0, WC_PUF_BCH_T + 1); ret = wc_PufInit(&ctx); if (ret != 0) @@ -24135,9 +24138,19 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) ret = wc_PufReadSram(&ctx, tooNoisySram, sizeof(tooNoisySram)); if (ret != 0) return WC_TEST_RET_ENC_EC(ret); - if (wc_PufReconstruct(&ctx, helperBuf, WC_PUF_HELPER_BYTES) - != WC_NO_ERR_TRACE(PUF_RECONSTRUCT_E)) - return WC_TEST_RET_ENC_NC; + ret = wc_PufReconstruct(&ctx, helperBuf, WC_PUF_HELPER_BYTES); + if (ret == 0) { + /* decoded to some valid codeword - it must NOT be the enrolled + * identity (a wrong-but-stable key is still a failure) */ + if (wc_PufGetIdentity(&ctx, id2, sizeof(id2)) == 0 && + XMEMCMP(id1, id2, WC_PUF_ID_SZ) == 0) + return WC_TEST_RET_ENC_NC; + } + else if (ret != WC_NO_ERR_TRACE(PUF_RECONSTRUCT_E)) { + /* the only acceptable failure here is the correction-limit + * rejection; any other code means something unrelated broke */ + return WC_TEST_RET_ENC_EC(ret); + } } /* ---- Test 5: Bad argument checks ---- */ @@ -24177,6 +24190,23 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) != WC_NO_ERR_TRACE(PUF_DERIVE_KEY_E)) return WC_TEST_RET_ENC_NC; + /* ---- Test 7: wc_PufGetParams reports the compiled-in profile ---- * + * This also catches a build-system mismatch where the library and the + * including application disagree on the selected profile. */ + { + int pm = 0, pn = 0, pk = 0, pt = 0, pnc = 0; + ret = wc_PufGetParams(&pm, &pn, &pk, &pt, &pnc); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (pm != WC_PUF_BCH_M || pn != WC_PUF_BCH_N || pk != WC_PUF_BCH_K || + pt != WC_PUF_BCH_T || pnc != WC_PUF_NUM_CODEWORDS) + return WC_TEST_RET_ENC_NC; + /* every output is optional, but an all-NULL call is a usage error */ + if (wc_PufGetParams(NULL, NULL, NULL, NULL, NULL) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + return WC_TEST_RET_ENC_NC; + } + return 0; #else return 0; diff --git a/wolfssl/wolfcrypt/puf.h b/wolfssl/wolfcrypt/puf.h index 855848574fe..61d554e113d 100644 --- a/wolfssl/wolfcrypt/puf.h +++ b/wolfssl/wolfcrypt/puf.h @@ -24,9 +24,23 @@ \brief SRAM PUF (Physically Unclonable Function) support for wolfCrypt. Derives device-unique cryptographic keys from the power-on state of SRAM - memory using a BCH(127,64,t=10) fuzzy extractor with HKDF key derivation. + memory using a configurable BCH(127,k,t) fuzzy extractor over GF(2^7) with + HKDF key derivation. - Build: ./configure --enable-puf (auto-enables HKDF) + Configuration (override in user_settings.h or via CPPFLAGS): + - WC_PUF_BCH_T selects the error-correction strength / BCH profile: + t=7 (k=78), t=10 (k=64, default), t=13 (k=50), t=15 (k=36). Higher t + corrects more bit flips per 127-bit codeword at the cost of entropy. + - WC_PUF_NUM_CODEWORDS (default 16) trades raw SRAM footprint and helper + data size against total derived-key entropy. + + Enrollment and reconstruction MUST use identical WC_PUF_BCH_T and + WC_PUF_NUM_CODEWORDS (and the same hash); a mismatch silently produces a + wrong key. See WC_PUF_PROFILE_ID and wc_PufGetParams(). + + Build: ./configure --enable-puf[=small|balanced|strong] (auto-enables + HKDF). CMake: -DWOLFSSL_PUF=yes with + -DWOLFSSL_PUF_PROFILE=small|balanced|strong. For a bare-metal example (tested on NUCLEO-H563ZI), see: https://github.com/wolfSSL/wolfssl-examples/tree/master/puf @@ -52,29 +66,107 @@ extern "C" { #endif -/* BCH(127,64,t=10) parameters */ +/* ------------------------------------------------------------------------ * + * Configuration knobs (override in user_settings.h or via CPPFLAGS). * + * * + * WC_PUF_NUM_CODEWORDS: number of BCH codewords. Trades SRAM footprint and * + * helper-data size against derived-key entropy. Default 16. * + * WC_PUF_BCH_T: BCH error-correction capability (bit flips corrected per * + * 127-bit codeword). Selects a BCH(127,k,t) profile. Supported values: * + * t=7 (k=78) smaller parity, high entropy, less correction * + * t=10 (k=64) default, wire-compatible with prior releases * + * t=13 (k=50) stronger correction for noisier SRAM * + * t=15 (k=36) strongest supported * + * * + * IMPORTANT: enrollment and reconstruction MUST use identical * + * WC_PUF_BCH_T and WC_PUF_NUM_CODEWORDS (and the same hash). A mismatch * + * silently produces a wrong key. Persist WC_PUF_PROFILE_ID (or the values * + * from wc_PufGetParams) with the helper data and compare on reconstruct. * + * ------------------------------------------------------------------------ */ +#ifndef WC_PUF_NUM_CODEWORDS + #define WC_PUF_NUM_CODEWORDS 16 +#endif +#ifndef WC_PUF_BCH_T + #define WC_PUF_BCH_T 10 +#endif + +/* Fixed field: GF(2^7), codeword length n = 127 */ #define WC_PUF_BCH_M 7 /* GF(2^7) */ #define WC_PUF_BCH_N 127 /* codeword length */ -#define WC_PUF_BCH_K 64 /* message length */ -#define WC_PUF_BCH_T 10 /* error correction capability */ -/* PUF dimensions */ -#define WC_PUF_NUM_CODEWORDS 16 /* 16 codewords */ -#define WC_PUF_RAW_BITS 2048 /* 16 x 128 bits (rounded up for storage) */ -#define WC_PUF_RAW_BYTES (WC_PUF_RAW_BITS / 8) /* 256 bytes */ -#define WC_PUF_STABLE_BITS 1024 /* 16 x 64 message bits */ -#define WC_PUF_STABLE_BYTES (WC_PUF_STABLE_BITS / 8) /* 128 bytes */ +/* Profile ladder: WC_PUF_BCH_T -> message length k. Only valid narrow-sense + * BCH(127) designed-distance profiles are accepted; other t values are a + * compile error rather than a silently wrong code. */ +#if WC_PUF_BCH_T == 7 + #define WC_PUF_BCH_K 78 +#elif WC_PUF_BCH_T == 10 + #define WC_PUF_BCH_K 64 +#elif WC_PUF_BCH_T == 13 + #define WC_PUF_BCH_K 50 +#elif WC_PUF_BCH_T == 15 + #define WC_PUF_BCH_K 36 +#else + #error "Unsupported WC_PUF_BCH_T; use 7, 10, 13, or 15 (see puf.h)" +#endif + +/* Generator-polynomial degree = parity bits per codeword = n - k */ +#define WC_PUF_BCH_DEG (WC_PUF_BCH_N - WC_PUF_BCH_K) + +#if WC_PUF_NUM_CODEWORDS < 1 + #error "WC_PUF_NUM_CODEWORDS must be >= 1" +#endif +/* WC_PUF_PROFILE_ID packs the codeword count into a 12-bit field; bound it so + * the fingerprint stays unique (and representable) for every valid build. Note + * this is a field-width limit, not a recommendation: wc_PufCtx embeds rawSram, + * helperData and stableBits by value, so it grows ~30 bytes per codeword and + * large counts are impractical on the small-RAM targets this module targets. */ +#if WC_PUF_NUM_CODEWORDS > 4095 + #error "WC_PUF_NUM_CODEWORDS must be <= 4095 (WC_PUF_PROFILE_ID field)" +#endif + +/* Per-codeword byte sizes */ +#define WC_PUF_CW_BYTES ((WC_PUF_BCH_N + 7) / 8) /* 16 for n=127 */ +#define WC_PUF_MSG_BYTES ((WC_PUF_BCH_K + 7) / 8) +#define WC_PUF_PARITY_BYTES ((WC_PUF_BCH_DEG + 7) / 8) + +/* Raw SRAM readout: 128-bit stride per codeword (n=127 fits in 128 bits) */ +#define WC_PUF_RAW_BITS (WC_PUF_NUM_CODEWORDS * 128) +#define WC_PUF_RAW_BYTES (WC_PUF_RAW_BITS / 8) -/* Helper data: 16 codewords x 127 bits, packed into bytes */ +/* Reconstructed stable bits: k message bits per codeword, bit-packed */ +#define WC_PUF_STABLE_BITS (WC_PUF_NUM_CODEWORDS * WC_PUF_BCH_K) +#define WC_PUF_STABLE_BYTES ((WC_PUF_STABLE_BITS + 7) / 8) + +/* Helper data: full n-bit codeword mask per codeword, bit-packed */ #define WC_PUF_HELPER_BITS (WC_PUF_NUM_CODEWORDS * WC_PUF_BCH_N) -#define WC_PUF_HELPER_BYTES ((WC_PUF_HELPER_BITS + 7) / 8) /* 254 bytes */ +#define WC_PUF_HELPER_BYTES ((WC_PUF_HELPER_BITS + 7) / 8) -/* Output key size */ +/* Recommended/default derived key size (HKDF output length is selectable) */ #define WC_PUF_KEY_SZ 32 /* 256-bit derived key */ /* Identity hash size (SHA-256 or SHA3-256 with WC_PUF_SHA3) */ #define WC_PUF_ID_SZ 32 +/* Hash-selection bit for the profile fingerprint (0 = SHA-256, 1 = SHA3-256). + * A device enrolled and reconstructed with different hashes yields a silently + * wrong key, so the hash must be part of the fingerprint. */ +#ifdef WC_PUF_SHA3 + #define WC_PUF_HASH_ID 1 +#else + #define WC_PUF_HASH_ID 0 +#endif + +/* Compact profile fingerprint the application can persist next to its helper + * data and compare before reconstruction to detect a build mismatch. Packs + * (hash, m, t, k, num_codewords); n is fixed at 127. Applications using the + * wc_PufGetParams() accessor can read this macro directly to record it. */ +#define WC_PUF_PROFILE_ID \ + (((word32)WC_PUF_HASH_ID << 31) | \ + ((word32)WC_PUF_BCH_M << 28) | \ + ((word32)WC_PUF_BCH_T << 20) | \ + ((word32)WC_PUF_BCH_K << 12) | \ + ((word32)(WC_PUF_NUM_CODEWORDS) & 0xFFFU)) + /* Flags for wc_PufCtx.flags */ #define WC_PUF_FLAG_ENROLLED 0x01 #define WC_PUF_FLAG_READY 0x02 @@ -101,6 +193,8 @@ WOLFSSL_API int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, WOLFSSL_API int wc_PufDeriveKey(wc_PufCtx* ctx, const byte* info, word32 infoSz, byte* key, word32 keySz); WOLFSSL_API int wc_PufGetIdentity(wc_PufCtx* ctx, byte* id, word32 idSz); +WOLFSSL_API int wc_PufGetParams(int* m, int* n, int* k, int* t, + int* numCodewords); WOLFSSL_API int wc_PufZeroize(wc_PufCtx* ctx); #ifdef WOLFSSL_PUF_TEST