1
0
mirror of https://github.com/wolfSSL/wolfssl.git synced 2026-10-24 04:09:07 +03:00

wolfCrypt SRAM PUF: configurable error correction and footprint

This commit is contained in:
David Garske
2026-08-03 14:20:52 -07:00
committed by Daniele Lacamera
parent e71b086fc8
commit 14a72c96e4
11 changed files with 1143 additions and 187 deletions

View File

@@ -18,18 +18,40 @@ 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"
# Odd codeword count: every other entry has k*codewords a multiple
# of 8, so the byte-rounded tails of stableBits/helperData carry no
# padding and the copyBits trailing-bit branch never runs. 3 x k=78
# (234 bits) and 3 x n=127 (381 bits) both leave pad bits.
- "--enable-puf=small --enable-puf-test CPPFLAGS=-DWC_PUF_NUM_CODEWORDS=3"
# Opt-in compact helper layout (parity bits only).
- "--enable-puf=strong --enable-puf-test CPPFLAGS=-DWC_PUF_HELPER_COMPACT"
steps:
- uses: actions/checkout@v5
name: Checkout wolfSSL
- name: Validate BCH generator polynomials against puf.c
run: python3 scripts/puf_bch_genpoly.py --check
- name: Build and test PUF
run: |
./autogen.sh
./configure --enable-puf --enable-puf-test
./configure ${{ matrix.config }}
make
./wolfcrypt/test/testwolfcrypt
@@ -39,3 +61,30 @@ jobs:
if [ -f test-suite.log ] ; then
cat test-suite.log
fi
puf_cmake_options:
name: PUF CMake options.h propagation
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
runs-on: ubuntu-24.04
timeout-minutes: 6
steps:
- uses: actions/checkout@v5
name: Checkout wolfSSL
# wc_PufCtx is sized from these macros, so if a profile fails to reach
# the generated options.h the application and library disagree on
# sizeof(wc_PufCtx). Assert they are actually emitted.
- name: Configure with a non-default profile
run: |
cmake -S . -B build_puf -DWOLFSSL_PUF=yes \
-DWOLFSSL_PUF_PROFILE=strong \
-DWOLFSSL_PUF_NUM_CODEWORDS=32
- name: Assert profile reached options.h
run: |
grep -q '^#define WC_PUF_BCH_T 13$' build_puf/wolfssl/options.h
grep -q '^#define WC_PUF_NUM_CODEWORDS 32$' build_puf/wolfssl/options.h
grep -q '^#define WOLFSSL_PUF$' build_puf/wolfssl/options.h
- name: Build
run: cmake --build build_puf -j

View File

@@ -770,6 +770,7 @@ WC_NO_VERBOSE_RNG
WC_PKCS11_FIND_WITH_ID_ONLY
WC_PKCS12_PBKDF_USING_MP_API
WC_PROTECT_ENCRYPTED_MEM
WC_PUF_HELPER_COMPACT
WC_PUF_SHA3
WC_RNG_BANK_NO_DEFAULT_SUPPORT
WC_RNG_BLOCKING

View File

@@ -2326,10 +2326,47 @@ 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")
# Codeword count. Plain cache STRING (not add_option) so a number is kept
# rather than reduced to yes/no. The cache name deliberately differs from
# the C macro: the loop that turns WOLFSSL_DEFINITIONS into CMake vars
# before configure_file() would otherwise clobber this cache entry.
# NOTE: setting WC_PUF_NUM_CODEWORDS through raw CMAKE_C_FLAGS bypasses
# WOLFSSL_DEFINITIONS and so can never reach the generated options.h,
# leaving the application and library with different sizeof(wc_PufCtx).
# Use this option instead.
set(WOLFSSL_PUF_NUM_CODEWORDS "" CACHE STRING
"Number of PUF BCH codewords (1-4095, default: 16)")
list(APPEND WOLFSSL_DEFINITIONS
"-DWOLFSSL_PUF"
"-DWOLFSSL_PUF_SRAM"
"-DHAVE_HKDF")
if(WOLFSSL_PUF_NUM_CODEWORDS)
if(NOT WOLFSSL_PUF_NUM_CODEWORDS MATCHES "^[0-9]+$" OR
WOLFSSL_PUF_NUM_CODEWORDS LESS 1 OR
WOLFSSL_PUF_NUM_CODEWORDS GREATER 4095)
message(FATAL_ERROR
"WOLFSSL_PUF_NUM_CODEWORDS must be an integer from 1 to 4095")
endif()
list(APPEND WOLFSSL_DEFINITIONS
"-DWC_PUF_NUM_CODEWORDS=${WOLFSSL_PUF_NUM_CODEWORDS}")
endif()
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()

View File

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

View File

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

View File

@@ -8596,16 +8596,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
@@ -13976,7 +13991,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 "---"

View File

@@ -1,6 +1,22 @@
/*!
\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. Only the (n - k) parity bits per codeword carry
information; the leading k bits of each helper codeword are identically
zero, so size OTP/flash accordingly. WC_PUF_HELPER_COMPACT stores only
those parity bits, shrinking helper data to 39-72% of the default. It
changes the stored format and so is opt-in; the default layout stays
compatible with helper data enrolled by wolfSSL 5.9.2.
Enrollment and reconstruction must use identical WC_PUF_BCH_T and
WC_PUF_NUM_CODEWORDS (and the same hash); persist WC_PUF_PROFILE_ID with
the helper data at enrollment and pass it to wc_PufReconstructEx, or
compare it against wc_PufGetProfileId(), to catch a build mismatch.
For a complete bare-metal example (tested on NUCLEO-H563ZI), see
https://github.com/wolfSSL/wolfssl-examples/tree/master/puf
*/
@@ -32,7 +48,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 +63,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 +76,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 +103,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 +186,118 @@ 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
\brief Return the profile fingerprint the LIBRARY was compiled with. The
WC_PUF_PROFILE_ID macro necessarily reflects the including application's
own build, so comparing the two detects a library/application mismatch -
including a differing hash selection - that no length check can see.
\return the library's WC_PUF_PROFILE_ID
_Example_
\code
if (wc_PufGetProfileId() != WC_PUF_PROFILE_ID) {
/* library and application were built with different PUF settings */
}
\endcode
\sa wc_PufGetParams
\sa wc_PufReconstructEx
*/
word32 wc_PufGetProfileId(void);
/*!
\ingroup PUF
\brief Copy out the enrollment helper data. Use this rather than reading
wc_PufCtx.helperData directly: the size varies with the selected profile
and with WC_PUF_HELPER_COMPACT.
\return 0 on success
\return BAD_FUNC_ARG if ctx or helper is NULL
\return PUF_ENROLL_E if the context is not enrolled, or helperSz is
less than WC_PUF_HELPER_BYTES
\param ctx pointer to an enrolled wc_PufCtx
\param helper output buffer for the helper data
\param helperSz size of helper in bytes (>= WC_PUF_HELPER_BYTES)
_Example_
\code
byte helper[WC_PUF_HELPER_BYTES];
wc_PufGetHelperData(&ctx, helper, sizeof(helper));
\endcode
\sa wc_PufEnroll
\sa wc_PufReconstructEx
*/
int wc_PufGetHelperData(wc_PufCtx* ctx, byte* helper, word32 helperSz);
/*!
\ingroup PUF
\brief Reconstruct as wc_PufReconstruct, but first check the caller's
stored profile id against the library's. Helper-data size does not depend
on t in the default layout, so the length check alone cannot detect helper
data enrolled by a differently configured build - which would otherwise
decode to a silently wrong key. Persist WC_PUF_PROFILE_ID alongside the
helper data at enrollment and pass it back here.
\return 0 on success
\return BAD_FUNC_ARG if ctx or helperData is NULL, or profileId does not
match the library's profile
\return PUF_RECONSTRUCT_E on failure (too many bit errors or helperSz
too small)
\param ctx pointer to wc_PufCtx (must have SRAM data loaded)
\param helperData pointer to helper data from a previous enrollment
\param helperSz size of helper data (>= WC_PUF_HELPER_BYTES)
\param profileId the WC_PUF_PROFILE_ID recorded at enrollment
_Example_
\code
wc_PufReconstructEx(&ctx, helper, sizeof(helper), savedProfileId);
\endcode
\sa wc_PufReconstruct
\sa wc_PufGetProfileId
*/
int wc_PufReconstructEx(wc_PufCtx* ctx, const byte* helperData,
word32 helperSz, word32 profileId);
/*!
\ingroup PUF
@@ -198,7 +330,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

303
scripts/puf_bch_genpoly.py Normal file
View File

@@ -0,0 +1,303 @@
#!/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
"""
import os
import re
import sys
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),
]
# The shipped tables live here; --check parses them out and diffs.
PUF_C = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "wolfcrypt", "src", "puf.c")
def parse_puf_c(path):
"""Extract {t: (genpoly_bytes, deg)} from the #if WC_PUF_BCH_T ladder in
wolfcrypt/src/puf.c. Parsing the real source is the point: a hardcoded
copy in this script would still pass if someone edited puf.c."""
src = open(path).read()
out = {}
# each arm: "WC_PUF_BCH_T == <t>" .. "bch_genpoly[..] = { .. };" .. "DEG <d>"
pat = re.compile(
r"WC_PUF_BCH_T\s*==\s*(\d+)\s*\n"
r".*?bch_genpoly\s*\[[^\]]*\]\s*=\s*\{(.*?)\}\s*;"
r".*?BCH_GENPOLY_DEG\s+(\d+)",
re.S)
for m in pat.finditer(src):
t = int(m.group(1))
body = m.group(2)
vals = [int(x, 16) for x in re.findall(r"0[xX]([0-9a-fA-F]{1,2})", body)]
out[t] = (vals, int(m.group(3)))
return out
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():
check = "--check" in sys.argv[1:]
# Sanity: exp table spot checks against known values.
assert EXP[1] == 0x02 and EXP[7] == 0x09, "GF(2^7) table mismatch"
shipped = parse_puf_c(PUF_C) if check else {}
if check and len(shipped) != len(PROFILES):
raise SystemExit("--check: parsed %d tables from %s, expected %d"
% (len(shipped), PUF_C, len(PROFILES)))
if not check:
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")
bad = 0
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))
# KAT self-check: the reference encoder must yield a genuine BCH
# codeword (all 2t syndromes zero), confirming both the generator
# polynomial and the encoder algebra for this profile.
kats = []
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)
if not syndromes_zero(cw, t):
raise SystemExit(
"t=%d: KAT codeword has nonzero syndrome" % t)
kats.append((msg_bits, cw))
if check:
# Diff the freshly derived table against what puf.c actually
# ships. This is the whole point of --check: a copy of the bytes
# kept inside this script would pass even if puf.c were edited.
if t not in shipped:
print("FAIL t=%d: no table found in puf.c" % t)
bad += 1
continue
got, gotdeg = shipped[t]
if got != bs:
print("FAIL t=%d: puf.c has %s, expected %s"
% (t, fmt_bytes(got), fmt_bytes(bs)))
bad += 1
elif gotdeg != deg:
print("FAIL t=%d: puf.c BCH_GENPOLY_DEG %d, expected %d"
% (t, gotdeg, deg))
bad += 1
else:
print("ok t=%d k=%d deg=%d %s" % (t, k, deg, fmt_bytes(bs)))
continue
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))
for msg_bits, cw in kats:
print("/* KAT msg=%s" % fmt_bytes(bits_to_hex(msg_bits)))
print(" cw =%s */" % fmt_bytes(bits_to_hex(cw)))
print()
if check:
if bad:
raise SystemExit("%d generator polynomial(s) in puf.c do NOT "
"match; regenerate or revert." % bad)
print("all %d shipped generator polynomials verified against %s"
% (len(PROFILES), os.path.normpath(PUF_C)))
if __name__ == "__main__":
main()

View File

@@ -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
@@ -156,13 +192,25 @@ static byte bch_syndrome_eval(const byte* codeword, int root)
return s;
}
/* Compute 2t syndromes S[1..2t] */
/* Compute 2t syndromes S[1..2t]. The codeword has GF(2) coefficients, so
* c(x)^2 = c(x^2) and therefore S_2i = S_i^2: only the t odd-index syndromes
* need a full evaluation pass, each even one is a single squaring. Syndromes
* dominate reconstruct and are computed twice per codeword (the second time
* for the post-correction recheck), so this roughly halves that cost. */
static void bch_syndromes(const byte* codeword, byte* syndromes)
{
byte s;
int i;
for (i = 1; i <= 2 * WC_PUF_BCH_T; i++) {
for (i = 1; i <= 2 * WC_PUF_BCH_T; i += 2) {
syndromes[i] = bch_syndrome_eval(codeword, i);
}
/* ascending, so S[i/2] is always already computed when S[i] is filled */
for (i = 2; i <= 2 * WC_PUF_BCH_T; i += 2) {
s = syndromes[i / 2];
syndromes[i] = (s == 0) ? (byte)0 :
gf_exp[(2 * (int)gf_log[s]) % GF_MASK];
}
}
/* ---- Berlekamp-Massey algorithm ---- */
@@ -266,92 +314,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
/* 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)]. */
/* 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 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 */
/* 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));
}
/* 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);
}
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 +446,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 +478,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,39 +487,25 @@ 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));
}
}
/* Store 127 bits into helper data at given bit offset */
/* Store the retained region of a helper codeword into helper data at the
* given bit offset. With WC_PUF_HELPER_COMPACT only the (n-k) parity bits are
* kept (the leading k bits are identically zero); otherwise the full n bits
* are stored, matching the layout shipped in 5.9.2. */
static void storeCodeword(byte* helper, int bitOffset, const byte* cw)
{
int i;
for (i = 0; i < WC_PUF_BCH_N; i++) {
setBit(helper, bitOffset + i, getBit(cw, i));
for (i = 0; i < WC_PUF_HELPER_LEN; i++) {
setBit(helper, bitOffset + i, getBit(cw, WC_PUF_HELPER_OFF + i));
}
}
@@ -489,10 +547,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 +577,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);
storeCodeword(ctx->helperData, i * WC_PUF_HELPER_LEN, 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 */
@@ -564,18 +624,25 @@ int wc_PufEnroll(wc_PufCtx* ctx)
return 0;
}
int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz)
int wc_PufReconstructEx(wc_PufCtx* ctx, const byte* helperData,
word32 helperSz, word32 profileId)
{
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");
WOLFSSL_ENTER("wc_PufReconstructEx");
if (ctx == NULL || helperData == NULL)
return BAD_FUNC_ARG;
/* Reject helper data from a different build before touching it: the
* length guard cannot catch this on its own, because helper size does not
* depend on t in the default layout. A mismatch would otherwise decode to
* a silently wrong key. */
if (profileId != (word32)WC_PUF_PROFILE_ID)
return BAD_FUNC_ARG;
if (helperSz < WC_PUF_HELPER_BYTES)
return PUF_RECONSTRUCT_E;
if (!(ctx->flags & WC_PUF_FLAG_SRAM_SET))
@@ -605,13 +672,14 @@ 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);
for (j = 0; j < WC_PUF_BCH_N; j++) {
setBit(helperCw, j, getBit(helperData, i * WC_PUF_BCH_N + j));
XMEMSET(helperCw, 0, WC_PUF_CW_BYTES);
for (j = 0; j < WC_PUF_HELPER_LEN; j++) {
setBit(helperCw, WC_PUF_HELPER_OFF + j,
getBit(helperData, i * WC_PUF_HELPER_LEN + 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 +701,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 */
@@ -658,6 +729,14 @@ int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz)
return 0;
}
int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz)
{
/* the profile id passed is the library's own, so the check inside the Ex
* variant is trivially satisfied and behaviour matches prior releases */
return wc_PufReconstructEx(ctx, helperData, helperSz,
(word32)WC_PUF_PROFILE_ID);
}
int wc_PufDeriveKey(wc_PufCtx* ctx, const byte* info, word32 infoSz,
byte* key, word32 keySz)
{
@@ -712,6 +791,63 @@ 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;
}
/* Report the profile fingerprint the LIBRARY was compiled with. The
* WC_PUF_PROFILE_ID macro necessarily reflects the calling application's own
* build, so comparing the two detects a library/application mismatch that no
* length check can see. */
word32 wc_PufGetProfileId(void)
{
WOLFSSL_ENTER("wc_PufGetProfileId");
return (word32)WC_PUF_PROFILE_ID;
}
/* Copy out the enrollment helper data. Applications should use this rather
* than reading wc_PufCtx.helperData directly: the size varies with the
* selected profile. */
int wc_PufGetHelperData(wc_PufCtx* ctx, byte* helper, word32 helperSz)
{
WOLFSSL_ENTER("wc_PufGetHelperData");
if (ctx == NULL || helper == NULL)
return BAD_FUNC_ARG;
if (!(ctx->flags & WC_PUF_FLAG_ENROLLED))
return PUF_ENROLL_E;
if (helperSz < WC_PUF_HELPER_BYTES)
return PUF_ENROLL_E;
XMEMCPY(helper, ctx->helperData, WC_PUF_HELPER_BYTES);
return 0;
}
int wc_PufZeroize(wc_PufCtx* ctx)
{
WOLFSSL_ENTER("wc_PufZeroize");
@@ -741,4 +877,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 */

View File

@@ -24274,6 +24274,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) && \
@@ -24284,50 +24312,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)
@@ -24388,14 +24390,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)
@@ -24429,13 +24429,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)
@@ -24446,9 +24449,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 ---- */
@@ -24488,6 +24501,128 @@ 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;
}
/* ---- Test 8: new accessors and the checked reconstruct path ---- */
{
byte helper2[WC_PUF_HELPER_BYTES];
/* the library's compiled-in profile must agree with the macro the
* application sees; a disagreement means a build/packaging bug */
if (wc_PufGetProfileId() != (word32)WC_PUF_PROFILE_ID)
return WC_TEST_RET_ENC_NC;
/* Test 6 zeroized ctx, so re-enroll to get a live context */
ret = wc_PufInit(&ctx);
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
ret = wc_PufSetTestData(&ctx, testSram, sizeof(testSram));
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
ret = wc_PufReadSram(&ctx, testSram, sizeof(testSram));
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
ret = wc_PufEnroll(&ctx);
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
/* accessor must return exactly what enrollment produced */
ret = wc_PufGetHelperData(&ctx, helper2, sizeof(helper2));
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
if (XMEMCMP(helper2, helperBuf, WC_PUF_HELPER_BYTES) != 0)
return WC_TEST_RET_ENC_NC;
if (wc_PufGetHelperData(&ctx, helper2, WC_PUF_HELPER_BYTES - 1)
!= WC_NO_ERR_TRACE(PUF_ENROLL_E))
return WC_TEST_RET_ENC_NC;
if (wc_PufGetHelperData(NULL, helper2, sizeof(helper2))
!= WC_NO_ERR_TRACE(BAD_FUNC_ARG))
return WC_TEST_RET_ENC_NC;
/* checked reconstruct: a foreign profile id must be refused before
* the helper data is touched, and the matching id must behave
* exactly like the plain entry point */
ret = wc_PufInit(&ctx);
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
ret = wc_PufSetTestData(&ctx, testSram, sizeof(testSram));
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
ret = wc_PufReadSram(&ctx, testSram, sizeof(testSram));
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
if (wc_PufReconstructEx(&ctx, helperBuf, WC_PUF_HELPER_BYTES,
((word32)WC_PUF_PROFILE_ID) ^ 1u)
!= WC_NO_ERR_TRACE(BAD_FUNC_ARG))
return WC_TEST_RET_ENC_NC;
ret = wc_PufReconstructEx(&ctx, helperBuf, WC_PUF_HELPER_BYTES,
(word32)WC_PUF_PROFILE_ID);
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
ret = wc_PufGetIdentity(&ctx, id2, sizeof(id2));
if (ret != 0)
return WC_TEST_RET_ENC_EC(ret);
if (XMEMCMP(id1, id2, WC_PUF_ID_SZ) != 0)
return WC_TEST_RET_ENC_NC;
}
/* ---- Test 9: BCH encoder known-answer test ---- *
* Recover codeword 0 from the enrolled helper data (cw = raw XOR helper,
* the helper contributing zero across the message region) and compare it
* against a vector produced by the independent python reference encoder
* in scripts/puf_bch_genpoly.py. This pins the C encoder itself for the
* selected profile - including the non-byte-aligned k=78/50/36 cases -
* rather than only the generator polynomial table. */
{
WOLFSSL_SMALL_STACK_STATIC const byte katCw[WC_PUF_CW_BYTES] = {
#if WC_PUF_BCH_T == 7
0x0D, 0xBC, 0x4B, 0x1A, 0x89, 0x78, 0xC7, 0xA6,
0x05, 0xA5, 0x88, 0xBD, 0x7E, 0xB2, 0x94, 0x5E
#elif WC_PUF_BCH_T == 10
0x0D, 0xBC, 0x4B, 0x1A, 0x89, 0x78, 0xC7, 0xA6,
0xB6, 0x86, 0xF2, 0xC2, 0xE4, 0x10, 0x37, 0x3A
#elif WC_PUF_BCH_T == 13
0x0D, 0xBC, 0x4B, 0x1A, 0x89, 0x78, 0xD0, 0xC9,
0x4D, 0x15, 0x71, 0x8E, 0xEA, 0x96, 0x61, 0x4E
#elif WC_PUF_BCH_T == 15
0x0D, 0xBC, 0x4B, 0x1A, 0x8B, 0x1A, 0xD8, 0x4F,
0x92, 0x24, 0xB1, 0x2D, 0x79, 0x39, 0x09, 0x40
#endif
};
byte gotCw[WC_PUF_CW_BYTES];
byte rawBit, helBit;
int j, hb;
XMEMSET(gotCw, 0, sizeof(gotCw));
for (j = 0; j < WC_PUF_BCH_N; j++) {
rawBit = (testSram[j / 8] >> (7 - (j % 8))) & 1;
helBit = 0;
if (j >= WC_PUF_HELPER_OFF) {
hb = j - WC_PUF_HELPER_OFF;
helBit = (helperBuf[hb / 8] >> (7 - (hb % 8))) & 1;
}
if ((rawBit ^ helBit) != 0)
gotCw[j / 8] |= (byte)(1 << (7 - (j % 8)));
}
if (XMEMCMP(gotCw, katCw, WC_PUF_CW_BYTES) != 0)
return WC_TEST_RET_ENC_NC;
}
return 0;
#else
return 0;

View File

@@ -24,9 +24,28 @@
\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().
WC_PUF_HELPER_COMPACT (opt-in) stores only the parity bits of the helper
data, shrinking it to 39-72% of the default size. It changes the stored
format, so the default keeps the layout used by wolfSSL 5.9.2.
Build: ./configure --enable-puf[=small|balanced|strong|strongest]
(auto-enables HKDF). CMake: -DWOLFSSL_PUF=yes with
-DWOLFSSL_PUF_PROFILE=small|balanced|strong|strongest and
-DWOLFSSL_PUF_NUM_CODEWORDS=<n>.
For a bare-metal example (tested on NUCLEO-H563ZI), see:
https://github.com/wolfSSL/wolfssl-examples/tree/master/puf
@@ -52,29 +71,132 @@
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
/* Helper data: 16 codewords x 127 bits, packed into bytes */
#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 */
/* Generator-polynomial degree = parity bits per codeword = n - k */
#define WC_PUF_BCH_DEG (WC_PUF_BCH_N - WC_PUF_BCH_K)
/* Output key size */
#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)
/* 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, bit-packed. Only the (n - k) parity bits per codeword carry
* information: helper = raw XOR encoded-codeword, and the systematic message
* part of the codeword is a verbatim copy of the raw bits it came from, so the
* leading k bits are identically zero. Size OTP/flash for that, not for n.
*
* WC_PUF_HELPER_COMPACT stores only the parity region. It changes the stored
* format, so it is opt-in: the default layout stays compatible with helper
* data enrolled by wolfSSL 5.9.2. The choice is encoded in
* WC_PUF_PROFILE_ID so a mismatch is detectable. */
#ifdef WC_PUF_HELPER_COMPACT
#define WC_PUF_HELPER_OFF WC_PUF_BCH_K /* first codeword bit stored */
#define WC_PUF_HELPER_LEN WC_PUF_BCH_DEG /* bits stored per codeword */
#define WC_PUF_HELPER_ID 1
#else
#define WC_PUF_HELPER_OFF 0
#define WC_PUF_HELPER_LEN WC_PUF_BCH_N
#define WC_PUF_HELPER_ID 0
#endif
#define WC_PUF_HELPER_BITS (WC_PUF_NUM_CODEWORDS * WC_PUF_HELPER_LEN)
#define WC_PUF_HELPER_BYTES ((WC_PUF_HELPER_BITS + 7) / 8)
/* 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, helper format, m, t, num_codewords); n is fixed at 127 and k is
* fully determined by t, so neither needs its own field. Layout:
* bit 31 hash select (0 = SHA-256, 1 = SHA3-256)
* bit 30 helper format (0 = full codeword, 1 = compact parity-only)
* bits 27-29 m (GF exponent)
* bits 19-26 t (error-correction capability)
* bits 12-18 reserved
* bits 0-11 codeword count
* This macro necessarily reflects the INCLUDING APPLICATION's build; use
* wc_PufGetProfileId() to read what the library itself was compiled with. */
#define WC_PUF_PROFILE_ID \
(((word32)WC_PUF_HASH_ID << 31) | \
((word32)WC_PUF_HELPER_ID << 30) | \
((word32)WC_PUF_BCH_M << 27) | \
((word32)WC_PUF_BCH_T << 19) | \
((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 +223,13 @@ 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 word32 wc_PufGetProfileId(void);
WOLFSSL_API int wc_PufGetHelperData(wc_PufCtx* ctx, byte* helper,
word32 helperSz);
WOLFSSL_API int wc_PufReconstructEx(wc_PufCtx* ctx, const byte* helperData,
word32 helperSz, word32 profileId);
WOLFSSL_API int wc_PufZeroize(wc_PufCtx* ctx);
#ifdef WOLFSSL_PUF_TEST