From baab15c923ce424533fa356e18d2404236df2107 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 24 Mar 2026 13:47:26 -0700 Subject: [PATCH] wolfCrypt SRAM PUF Support Add SRAM PUF (Physically Unclonable Function) support to 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. - **wolfCrypt PUF API** (`wolfcrypt/src/puf.c`, `wolfssl/wolfcrypt/puf.h`) - `wc_PufInit`, `wc_PufReadSram`, `wc_PufEnroll`, `wc_PufReconstruct` - `wc_PufDeriveKey` (HKDF-SHA256), `wc_PufGetIdentity` (SHA-256 device fingerprint) - `wc_PufZeroize` (secure context cleanup) - `wc_PufSetTestData` (synthetic SRAM for testing without hardware) - **BCH(127,64,t=10) error-correcting codec** - corrects up to 10 bit flips per 127-bit codeword across 16 codewords - **`WC_PUF_SHA3` build option** - select SHA3-256 instead of SHA-256 for identity hash and HKDF (default: SHA-256) - **Precomputed GF(2^7) tables** - `const` arrays in `.rodata` (no runtime init, thread-safe, flash-resident on embedded) - `./configure --enable-puf` (auto-enables HKDF dependency) - CMake: `WOLFSSL_PUF=yes` - `WOLFSSL_USER_SETTINGS`: define `WOLFSSL_PUF` and `WOLFSSL_PUF_SRAM` - See wolfssl-examples/puf for example implementation on STM32 NUCLEO-H563ZI (Cortex-M33, STM32H563ZI) - Supports test mode (synthetic SRAM) - Builds to ~13KB `.elf` - Tested on NUCLEO-H563ZI: enrollment, noisy reconstruction, key derivation all pass - `.github/workflows/puf.yml`: host build + test workflow for PUF feature - Doxygen API docs for all 8 public functions - PUF group added to `doxygen_groups.h` --- .github/workflows/puf.yml | 37 + .wolfssl_known_macro_extras | 1 + CMakeLists.txt | 12 + README.md | 5 + configure.ac | 16 + .../header_files/doxygen_groups.h | 1 + doc/dox_comments/header_files/puf.h | 211 ++++++ src/include.am | 4 + wolfcrypt/src/error.c | 18 + wolfcrypt/src/puf.c | 632 ++++++++++++++++++ wolfcrypt/test/test.c | 197 ++++++ wolfssl/wolfcrypt/error-crypt.h | 12 +- wolfssl/wolfcrypt/puf.h | 108 +++ 13 files changed, 1252 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/puf.yml create mode 100644 doc/dox_comments/header_files/puf.h create mode 100644 wolfcrypt/src/puf.c create mode 100644 wolfssl/wolfcrypt/puf.h diff --git a/.github/workflows/puf.yml b/.github/workflows/puf.yml new file mode 100644 index 00000000000..f7096e855b1 --- /dev/null +++ b/.github/workflows/puf.yml @@ -0,0 +1,37 @@ +name: PUF Tests + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +# END OF COMMON SECTION + +jobs: + puf_host_test: + name: PUF host test + if: github.repository_owner == 'wolfssl' + runs-on: ubuntu-24.04 + timeout-minutes: 6 + steps: + - uses: actions/checkout@v4 + name: Checkout wolfSSL + + - name: Build and test PUF + run: | + ./autogen.sh + ./configure --enable-puf + make + ./wolfcrypt/test/testwolfcrypt + + - name: Print errors + if: ${{ failure() }} + run: | + if [ -f test-suite.log ] ; then + cat test-suite.log + fi diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 3bcb317b8d8..d6b95474387 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -646,6 +646,7 @@ WC_NO_VERBOSE_RNG WC_PKCS11_FIND_WITH_ID_ONLY WC_PKCS12_PBKDF_USING_MP_API WC_PROTECT_ENCRYPTED_MEM +WC_PUF_SHA3 WC_RNG_BLOCKING WC_RSA_NONBLOCK WC_RSA_NONBLOCK_TIME diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c7716f9b15..90e789c6de2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1730,6 +1730,18 @@ endif() # TODO: - XCHACHA +# SRAM PUF +add_option("WOLFSSL_PUF" + "Enable SRAM PUF support (default: disabled)" + "no" "yes;no") + +if(WOLFSSL_PUF) + list(APPEND WOLFSSL_DEFINITIONS + "-DWOLFSSL_PUF" + "-DWOLFSSL_PUF_SRAM" + "-DWOLFSSL_PUF_TEST") +endif() + # Hash DRBG add_option("WOLFSSL_HASH_DRBG" "Enable Hash DRBG support (default: enabled)" diff --git a/README.md b/README.md index c50f0672f1e..164a11b59fe 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ certificate #3389). FIPS 140-3 validated (Certificate #4718). For additional 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`) and an example exists at +[SRAM PUF](https://github.com/wolfSSL/wolfssl-examples/tree/master/puf) + + ## Why Choose wolfSSL? There are many reasons to choose wolfSSL as your embedded, desktop, mobile, or diff --git a/configure.ac b/configure.ac index 97445a22091..1f52e517507 100644 --- a/configure.ac +++ b/configure.ac @@ -7172,6 +7172,20 @@ then AM_CFLAGS="$AM_CFLAGS -DHAVE_ASCON" fi +# PUF +AC_ARG_ENABLE([puf], + [AS_HELP_STRING([--enable-puf],[Enable SRAM PUF support (default: disabled)])], + [ ENABLED_PUF=$enableval ], + [ ENABLED_PUF=no ] + ) + +if test "$ENABLED_PUF" = "yes" +then + AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_PUF -DWOLFSSL_PUF_SRAM -DWOLFSSL_PUF_TEST" + AS_IF([test "$ENABLED_HKDF" != "yes"], + [ENABLED_HKDF="yes"; AM_CFLAGS="$AM_CFLAGS -DHAVE_HKDF"]) +fi + # Hash DRBG AC_ARG_ENABLE([hashdrbg], [AS_HELP_STRING([--enable-hashdrbg],[Enable Hash DRBG support (default: enabled)])], @@ -11548,6 +11562,7 @@ AM_CONDITIONAL([BUILD_CHACHA],[test "x$ENABLED_CHACHA" = "xyes" || test "x$ENABL AM_CONDITIONAL([BUILD_CHACHA_NOASM],[test "$ENABLED_CHACHA" = "noasm"]) AM_CONDITIONAL([BUILD_XCHACHA],[test "x$ENABLED_XCHACHA" = "xyes" || test "x$ENABLED_USERSETTINGS" = "xyes"]) AM_CONDITIONAL([BUILD_ASCON],[test "x$ENABLED_ASCON" = "xyes" || test "x$ENABLED_USERSETTINGS" = "xyes"]) +AM_CONDITIONAL([BUILD_PUF],[test "x$ENABLED_PUF" = "xyes" || test "x$ENABLED_USERSETTINGS" = "xyes"]) AM_CONDITIONAL([BUILD_SM2],[test "x$ENABLED_SM2" != "xno" || test "x$ENABLED_USERSETTINGS" = "xyes"]) AM_CONDITIONAL([BUILD_SM3],[test "x$ENABLED_SM3" != "xno" || test "x$ENABLED_USERSETTINGS" = "xyes"]) AM_CONDITIONAL([BUILD_SM4],[test "x$ENABLED_SM4" != "xno" || test "x$ENABLED_USERSETTINGS" = "xyes"]) @@ -12214,6 +12229,7 @@ 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" +echo " * PUF: $ENABLED_PUF" echo "" echo "---" diff --git a/doc/dox_comments/header_files/doxygen_groups.h b/doc/dox_comments/header_files/doxygen_groups.h index 5cac25e2ddd..b5ca3335c59 100644 --- a/doc/dox_comments/header_files/doxygen_groups.h +++ b/doc/dox_comments/header_files/doxygen_groups.h @@ -202,6 +202,7 @@ \defgroup PKCS11 Algorithms - PKCS11 \defgroup Password Algorithms - Password Based \defgroup Poly1305 Algorithms - Poly1305 + \defgroup PUF Algorithms - PUF \defgroup RIPEMD Algorithms - RIPEMD \defgroup RSA Algorithms - RSA \defgroup SHA Algorithms - SHA 128/224/256/384/512 diff --git a/doc/dox_comments/header_files/puf.h b/doc/dox_comments/header_files/puf.h new file mode 100644 index 00000000000..a16e25a399e --- /dev/null +++ b/doc/dox_comments/header_files/puf.h @@ -0,0 +1,211 @@ +/*! + \ingroup PUF + + For a complete bare-metal example (tested on NUCLEO-H563ZI), see + https://github.com/wolfSSL/wolfssl-examples/tree/master/puf +*/ + +/*! + \ingroup PUF + + \brief Initialize a wc_PufCtx structure, zeroing all fields. + Must be called before any other PUF operations. + + \return 0 on success + \return BAD_FUNC_ARG if ctx is NULL + + \param ctx pointer to wc_PufCtx structure to initialize + + _Example_ + \code + wc_PufCtx ctx; + ret = wc_PufInit(&ctx); + \endcode + + \sa wc_PufReadSram + \sa wc_PufEnroll + \sa wc_PufZeroize +*/ +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. + + \return 0 on success + \return BAD_FUNC_ARG if ctx or sramAddr is NULL + \return PUF_READ_E if sramSz < WC_PUF_RAW_BYTES + + \param ctx pointer to wc_PufCtx structure + \param sramAddr pointer to raw SRAM memory region + \param sramSz size of SRAM buffer (must be >= WC_PUF_RAW_BYTES) + + _Example_ + \code + __attribute__((section(".puf_sram"))) + static volatile uint8_t puf_sram[256]; + wc_PufReadSram(&ctx, (const byte*)puf_sram, sizeof(puf_sram)); + \endcode + + \sa wc_PufInit + \sa wc_PufEnroll + \sa wc_PufReconstruct +*/ +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. + + \return 0 on success + \return BAD_FUNC_ARG if ctx is NULL + \return PUF_ENROLL_E if enrollment fails + + \param ctx pointer to wc_PufCtx (must have SRAM data loaded) + + _Example_ + \code + wc_PufEnroll(&ctx); + XMEMCPY(helperData, ctx.helperData, WC_PUF_HELPER_BYTES); + \endcode + + \sa wc_PufReadSram + \sa wc_PufReconstruct + \sa wc_PufDeriveKey +*/ +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. + + \return 0 on success + \return BAD_FUNC_ARG if ctx or helperData is NULL + \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 previous enrollment + \param helperSz size of helper data (>= WC_PUF_HELPER_BYTES) + + _Example_ + \code + wc_PufReconstruct(&ctx, helperData, sizeof(helperData)); + \endcode + + \sa wc_PufEnroll + \sa wc_PufDeriveKey + \sa wc_PufGetIdentity +*/ +int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz); + +/*! + \ingroup PUF + + \brief Derive a cryptographic key from PUF stable bits using HKDF. + Uses SHA-256 by default, or SHA3-256 when WC_PUF_SHA3 is defined. + The info parameter provides domain separation for multiple keys. + Requires HAVE_HKDF. + + \return 0 on success + \return BAD_FUNC_ARG if ctx or key is NULL, or keySz is 0 + \return PUF_DERIVE_KEY_E if PUF not ready or HKDF fails + + \param ctx pointer to wc_PufCtx (must be enrolled or reconstructed) + \param info optional context info for domain separation (may be NULL) + \param infoSz size of info in bytes + \param key output buffer for derived key + \param keySz desired key size in bytes + + _Example_ + \code + byte key[32]; + const byte info[] = "my-app-key"; + wc_PufDeriveKey(&ctx, info, sizeof(info), key, sizeof(key)); + \endcode + + \sa wc_PufEnroll + \sa wc_PufReconstruct + \sa wc_PufGetIdentity +*/ +int wc_PufDeriveKey(wc_PufCtx* ctx, const byte* info, word32 infoSz, + byte* key, word32 keySz); + +/*! + \ingroup PUF + + \brief Retrieve the device identity hash (SHA-256 or SHA3-256 of stable + bits). Deterministic for a given device. + + \return 0 on success + \return BAD_FUNC_ARG if ctx or id is NULL + \return PUF_IDENTITY_E if PUF not ready or idSz < WC_PUF_ID_SZ + + \param ctx pointer to wc_PufCtx (must be enrolled or reconstructed) + \param id output buffer for identity hash + \param idSz size of id buffer (>= WC_PUF_ID_SZ, 32 bytes) + + _Example_ + \code + byte identity[WC_PUF_ID_SZ]; + wc_PufGetIdentity(&ctx, identity, sizeof(identity)); + \endcode + + \sa wc_PufEnroll + \sa wc_PufReconstruct + \sa wc_PufDeriveKey +*/ +int wc_PufGetIdentity(wc_PufCtx* ctx, byte* id, word32 idSz); + +/*! + \ingroup PUF + + \brief Securely zeroize all sensitive data in the PUF context using + ForceZero. Call when PUF is no longer needed. + + \return 0 on success + \return BAD_FUNC_ARG if ctx is NULL + + \param ctx pointer to wc_PufCtx to zeroize + + _Example_ + \code + wc_PufZeroize(&ctx); + \endcode + + \sa wc_PufInit +*/ +int wc_PufZeroize(wc_PufCtx* ctx); + +/*! + \ingroup PUF + + \brief Inject synthetic SRAM test data for testing without hardware. + Only available when WOLFSSL_PUF_TEST is defined. + + \return 0 on success + \return BAD_FUNC_ARG if ctx or data is NULL + \return PUF_READ_E if sz < WC_PUF_RAW_BYTES + + \param ctx pointer to wc_PufCtx + \param data pointer to synthetic SRAM data + \param sz size of data (>= WC_PUF_RAW_BYTES, 256 bytes) + + _Example_ + \code + byte testSram[WC_PUF_RAW_BYTES]; + wc_PufSetTestData(&ctx, testSram, sizeof(testSram)); + \endcode + + \sa wc_PufInit + \sa wc_PufReadSram +*/ +int wc_PufSetTestData(wc_PufCtx* ctx, const byte* data, word32 sz); diff --git a/src/include.am b/src/include.am index cb91fe84cc5..e959b8a2071 100644 --- a/src/include.am +++ b/src/include.am @@ -1352,6 +1352,10 @@ if BUILD_ASCON src_libwolfssl@LIBSUFFIX@_la_SOURCES += wolfcrypt/src/ascon.c endif +if BUILD_PUF +src_libwolfssl@LIBSUFFIX@_la_SOURCES += wolfcrypt/src/puf.c +endif + if !BUILD_INLINE src_libwolfssl@LIBSUFFIX@_la_SOURCES += wolfcrypt/src/misc.c endif diff --git a/wolfcrypt/src/error.c b/wolfcrypt/src/error.c index 431a3c996c0..b73ea1cd099 100644 --- a/wolfcrypt/src/error.c +++ b/wolfcrypt/src/error.c @@ -668,6 +668,24 @@ const char* wc_GetErrorString(int error) case SEQ_OVERFLOW_E: return "Sequence counter would overflow"; + case PUF_INIT_E: + return "PUF initialization failed"; + + case PUF_READ_E: + return "PUF SRAM read failed"; + + case PUF_ENROLL_E: + return "PUF enrollment failed"; + + case PUF_RECONSTRUCT_E: + return "PUF reconstruction failed"; + + case PUF_DERIVE_KEY_E: + return "PUF key derivation failed"; + + case PUF_IDENTITY_E: + return "PUF identity retrieval failed"; + case MAX_CODE_E: case WC_SPAN1_MIN_CODE_E: case MIN_CODE_E: diff --git a/wolfcrypt/src/puf.c b/wolfcrypt/src/puf.c new file mode 100644 index 00000000000..83504dd28f6 --- /dev/null +++ b/wolfcrypt/src/puf.c @@ -0,0 +1,632 @@ +/* puf.c + * + * 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 + */ + + +#include + +#ifdef WOLFSSL_PUF + +/* Currently only SRAM PUF is implemented. Other PUF types (ring-oscillator, + * arbiter) may be added in the future with their own guard macros. */ +#if !defined(WOLFSSL_PUF_SRAM) + #define WOLFSSL_PUF_SRAM +#endif + +/* PUF is not a FIPS-validated algorithm. It provides a + * hardware-derived key source that would need separate + * FIPS validation if used in a FIPS boundary. */ +#ifndef HAVE_FIPS + +#include +#include +#include + +#ifdef HAVE_HKDF + #include +#endif + +/* Hash algorithm selection: SHA3-256 or SHA-256 (default) */ +#ifdef WC_PUF_SHA3 + #include + #define WC_PUF_HASH_TYPE WC_SHA3_256 + #define wc_PufHashDirect wc_Sha3_256Hash +#else + #define WC_PUF_HASH_TYPE WC_SHA256 + #define wc_PufHashDirect wc_Sha256Hash +#endif + +#ifdef NO_INLINE + #include +#else + #define WOLFSSL_MISC_INCLUDED + #include +#endif + +/* ========================================================================== */ +/* BCH(127,64,t=10) codec over GF(2^7) */ +/* ========================================================================== */ + +/* GF(2^7) arithmetic with primitive polynomial p(x) = x^7 + x^3 + 1 (0x89) */ +#define GF_M 7 +#define GF_SIZE (1 << GF_M) /* 128 */ +#define GF_MASK (GF_SIZE - 1) /* 127 */ + +/* Precomputed GF(2^7) exp table: gf_exp[i] = alpha^i for i=0..127 + * Generated with primitive polynomial 0x89 (x^7 + x^3 + 1). + * gf_exp[127] wraps to gf_exp[0] = 1. */ +static const byte gf_exp[GF_SIZE] = { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x09, + 0x12, 0x24, 0x48, 0x19, 0x32, 0x64, 0x41, 0x0B, + 0x16, 0x2C, 0x58, 0x39, 0x72, 0x6D, 0x53, 0x2F, + 0x5E, 0x35, 0x6A, 0x5D, 0x33, 0x66, 0x45, 0x03, + 0x06, 0x0C, 0x18, 0x30, 0x60, 0x49, 0x1B, 0x36, + 0x6C, 0x51, 0x2B, 0x56, 0x25, 0x4A, 0x1D, 0x3A, + 0x74, 0x61, 0x4B, 0x1F, 0x3E, 0x7C, 0x71, 0x6B, + 0x5F, 0x37, 0x6E, 0x55, 0x23, 0x46, 0x05, 0x0A, + 0x14, 0x28, 0x50, 0x29, 0x52, 0x2D, 0x5A, 0x3D, + 0x7A, 0x7D, 0x73, 0x6F, 0x57, 0x27, 0x4E, 0x15, + 0x2A, 0x54, 0x21, 0x42, 0x0D, 0x1A, 0x34, 0x68, + 0x59, 0x3B, 0x76, 0x65, 0x43, 0x0F, 0x1E, 0x3C, + 0x78, 0x79, 0x7B, 0x7F, 0x77, 0x67, 0x47, 0x07, + 0x0E, 0x1C, 0x38, 0x70, 0x69, 0x5B, 0x3F, 0x7E, + 0x75, 0x63, 0x4F, 0x17, 0x2E, 0x5C, 0x31, 0x62, + 0x4D, 0x13, 0x26, 0x4C, 0x11, 0x22, 0x44, 0x01 +}; + +/* Precomputed GF(2^7) log table: gf_log[x] = log_alpha(x) for x=0..127 + * gf_log[0] is undefined (set to 0 for safety). */ +static const byte gf_log[GF_SIZE] = { + 0x00, 0x00, 0x01, 0x1F, 0x02, 0x3E, 0x20, 0x67, + 0x03, 0x07, 0x3F, 0x0F, 0x21, 0x54, 0x68, 0x5D, + 0x04, 0x7C, 0x08, 0x79, 0x40, 0x4F, 0x10, 0x73, + 0x22, 0x0B, 0x55, 0x26, 0x69, 0x2E, 0x5E, 0x33, + 0x05, 0x52, 0x7D, 0x3C, 0x09, 0x2C, 0x7A, 0x4D, + 0x41, 0x43, 0x50, 0x2A, 0x11, 0x45, 0x74, 0x17, + 0x23, 0x76, 0x0C, 0x1C, 0x56, 0x19, 0x27, 0x39, + 0x6A, 0x13, 0x2F, 0x59, 0x5F, 0x47, 0x34, 0x6E, + 0x06, 0x0E, 0x53, 0x5C, 0x7E, 0x1E, 0x3D, 0x66, + 0x0A, 0x25, 0x2D, 0x32, 0x7B, 0x78, 0x4E, 0x72, + 0x42, 0x29, 0x44, 0x16, 0x51, 0x3B, 0x2B, 0x4C, + 0x12, 0x58, 0x46, 0x6D, 0x75, 0x1B, 0x18, 0x38, + 0x24, 0x31, 0x77, 0x71, 0x0D, 0x5B, 0x1D, 0x65, + 0x57, 0x6C, 0x1A, 0x37, 0x28, 0x15, 0x3A, 0x4B, + 0x6B, 0x36, 0x14, 0x4A, 0x30, 0x70, 0x5A, 0x64, + 0x60, 0x61, 0x48, 0x62, 0x35, 0x49, 0x6F, 0x63 +}; + +/* GF multiplication */ +static WC_INLINE byte gf_mul(byte a, byte b) +{ + if (a == 0 || b == 0) + return 0; + return gf_exp[(gf_log[a] + gf_log[b]) % GF_MASK]; +} + +/* GF inverse */ +static WC_INLINE byte gf_inv(byte a) +{ + if (a == 0) + return 0; + return gf_exp[GF_MASK - gf_log[a]]; +} + +/* ---- BCH syndrome computation ---- */ + +/* Evaluate syndrome: S_root = c(alpha^root) where codeword bits are packed + * MSB-first. Bit at position j in the byte array corresponds to the + * coefficient of x^(N-1-j) in the codeword polynomial, so we evaluate + * using alpha^(root*(N-1-j)) to correctly compute c(alpha^root). */ +static byte bch_syndrome_eval(const byte* codeword, int root) +{ + byte s = 0; + int j; + + for (j = 0; j < WC_PUF_BCH_N; j++) { + int byteIdx = j / 8; + int bitIdx = 7 - (j % 8); + + if (codeword[byteIdx] & (1 << bitIdx)) { + /* coefficient of x^(N-1-j), evaluated at alpha^root */ + s ^= gf_exp[(root * (WC_PUF_BCH_N - 1 - j)) % GF_MASK]; + } + } + return s; +} + +/* Compute 2t syndromes S[1..2t] */ +static void bch_syndromes(const byte* codeword, byte* syndromes) +{ + int i; + for (i = 1; i <= 2 * WC_PUF_BCH_T; i++) { + syndromes[i] = bch_syndrome_eval(codeword, i); + } +} + +/* ---- Berlekamp-Massey algorithm ---- */ + +/* Find error locator polynomial sigma(x) from syndromes. + * sigma[] has degree <= t, coefficients in GF(2^7). + * Returns degree of sigma, or -1 on failure. */ +static int bch_berlekamp_massey(const byte* syndromes, byte* sigma) +{ + byte C[WC_PUF_BCH_T + 1]; /* current polynomial */ + byte B[WC_PUF_BCH_T + 1]; /* previous polynomial */ + byte T[WC_PUF_BCH_T + 1]; /* temp */ + int L = 0; /* current length */ + int m = 1; /* shift counter */ + byte b = 1; /* previous discrepancy */ + int n, i, degC; + + XMEMSET(C, 0, sizeof(C)); + XMEMSET(B, 0, sizeof(B)); + C[0] = 1; + B[0] = 1; + + for (n = 0; n < 2 * WC_PUF_BCH_T; n++) { + /* compute discrepancy d */ + byte d = syndromes[n + 1]; + for (i = 1; i <= L; i++) { + d ^= gf_mul(C[i], syndromes[n + 1 - i]); + } + + if (d == 0) { + m++; + } + else if (2 * L <= n) { + /* update: T(x) = C(x), C(x) -= (d/b)*x^m * B(x), B=T, L=n+1-L */ + byte coeff = gf_mul(d, gf_inv(b)); + XMEMCPY(T, C, sizeof(T)); + for (i = m; i <= WC_PUF_BCH_T; i++) { + C[i] ^= gf_mul(coeff, B[i - m]); + } + XMEMCPY(B, T, sizeof(B)); + L = n + 1 - L; + b = d; + m = 1; + } + else { + /* C(x) -= (d/b)*x^m * B(x) */ + byte coeff = gf_mul(d, gf_inv(b)); + for (i = m; i <= WC_PUF_BCH_T; i++) { + C[i] ^= gf_mul(coeff, B[i - m]); + } + m++; + } + } + + XMEMCPY(sigma, C, (WC_PUF_BCH_T + 1)); + + /* find degree */ + degC = 0; + for (i = WC_PUF_BCH_T; i >= 0; i--) { + if (sigma[i] != 0) { + degC = i; + break; + } + } + + if (degC > WC_PUF_BCH_T) + return -1; + + return degC; +} + +/* ---- Chien search: find error locations ---- */ + +/* Evaluate sigma at alpha^(-j) for j=0..126. Returns number of roots found. + * Error positions stored in errPos[] as byte-scan positions (MSB-first). + * Chien search root j maps to bit position (N-1-j) to match the MSB-first + * codeword layout used by the syndrome computation. */ +static int bch_chien_search(const byte* sigma, int deg, int* errPos) +{ + int count = 0; + int j; + + for (j = 0; j < WC_PUF_BCH_N; j++) { + byte val = 0; + int i; + for (i = 0; i <= deg; i++) { + if (sigma[i] != 0) { + /* sigma[i] * alpha^(-i*j) */ + int exp_val = (GF_MASK - ((i * j) % GF_MASK)) % GF_MASK; + val ^= gf_mul(sigma[i], gf_exp[exp_val]); + } + } + if (val == 0) { + if (count >= WC_PUF_BCH_T) + return -1; /* too many roots, protect errPos[] bounds */ + errPos[count] = WC_PUF_BCH_N - 1 - j; + count++; + } + } + + return count; +} + +/* ---- BCH encode: compute parity for 64-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. */ + +/* 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 +}; + +/* 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)]. */ +static void bch_encode(const byte* msg, byte* codeword) +{ + byte shift_reg[8]; /* 63-bit shift register for parity */ + int i, j; + + XMEMSET(shift_reg, 0, sizeof(shift_reg)); + + /* Process each of the 64 message bits */ + for (i = 0; i < WC_PUF_BCH_K; i++) { + int byteIdx = i / 8; + int bitIdx = 7 - (i % 8); + byte msgBit = (msg[byteIdx] >> bitIdx) & 1; + + /* feedback = msgBit XOR MSB of shift register */ + byte fb = msgBit ^ ((shift_reg[0] >> 6) & 1); + + /* shift register left by 1 */ + for (j = 0; j < 7; j++) { + shift_reg[j] = (byte)((shift_reg[j] << 1) | + (shift_reg[j + 1] >> 7)); + } + shift_reg[7] = (byte)(shift_reg[7] << 1); + + /* XOR with generator if feedback is 1 */ + if (fb) { + for (j = 0; j < 8; j++) { + shift_reg[j] ^= bch_genpoly[j]; + } + } + } + + /* 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 = i / 8; + int srcBit = 6 - (i % 8); + + /* Adjust: 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); + } + } +} + +/* ---- BCH decode ---- */ + +/* Decode 127-bit codeword, correct up to t=10 errors. + * Extracts 64-bit message into msg (8 bytes). + * Returns 0 on success, negative on uncorrectable error. */ +static int bch_decode(byte* codeword, byte* msg) +{ + byte syndr[2 * WC_PUF_BCH_T + 1]; + byte sigma[WC_PUF_BCH_T + 1]; + int errPos[WC_PUF_BCH_T]; + int deg, numErr; + int i; + int allZero = 1; + + bch_syndromes(codeword, syndr); + + /* check if all syndromes are zero (no errors) */ + for (i = 1; i <= 2 * WC_PUF_BCH_T; i++) { + if (syndr[i] != 0) { + allZero = 0; + break; + } + } + + if (allZero) { + /* no errors, extract message directly */ + XMEMCPY(msg, codeword, 8); + return 0; + } + + deg = bch_berlekamp_massey(syndr, sigma); + if (deg < 0) + return PUF_RECONSTRUCT_E; + + numErr = bch_chien_search(sigma, deg, errPos); + if (numErr != deg) + return PUF_RECONSTRUCT_E; /* number of roots must match degree */ + + /* correct errors by flipping bits */ + for (i = 0; i < numErr; i++) { + int pos = errPos[i]; + if (pos < WC_PUF_BCH_N) { + int byteIdx = pos / 8; + int bitIdx = 7 - (pos % 8); + codeword[byteIdx] ^= (byte)(1 << bitIdx); + } + } + + /* extract message (first 64 bits) */ + XMEMCPY(msg, codeword, 8); + return 0; +} + +/* ========================================================================== */ +/* 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); + 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 */ +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)); + } +} + + +int wc_PufInit(wc_PufCtx* ctx) +{ + if (ctx == NULL) + return BAD_FUNC_ARG; + + XMEMSET(ctx, 0, sizeof(wc_PufCtx)); + + return 0; +} + +int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, word32 sramSz) +{ + if (ctx == NULL || sramAddr == NULL) + return BAD_FUNC_ARG; + if (sramSz < WC_PUF_RAW_BYTES) + return PUF_READ_E; + +#ifdef WOLFSSL_PUF_TEST + if (ctx->testDataSet) { + /* rawSram already populated by wc_PufSetTestData */ + return 0; + } +#endif + + XMEMCPY(ctx->rawSram, sramAddr, WC_PUF_RAW_BYTES); + return 0; +} + +int wc_PufEnroll(wc_PufCtx* ctx) +{ + int i, ret; + + if (ctx == NULL) + return BAD_FUNC_ARG; + + XMEMSET(ctx->helperData, 0, WC_PUF_HELPER_BYTES); + XMEMSET(ctx->stableBits, 0, WC_PUF_STABLE_BYTES); + + for (i = 0; i < WC_PUF_NUM_CODEWORDS; i++) { + byte msg[8]; /* 64-bit message */ + byte cw[16]; /* 127-bit codeword */ + + /* extract 64 message bits from raw SRAM */ + int bitOff = i * 128; /* 128-bit stride for alignment */ + 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); + + /* encode message into BCH codeword */ + bch_encode(msg, cw); + + /* helper = raw XOR codeword (mask) */ + { + byte rawCw[16]; + byte helperCw[16]; + extractCodeword(ctx->rawSram, bitOff, rawCw); + XMEMSET(helperCw, 0, 16); + for (j = 0; j < 16; j++) { + helperCw[j] = rawCw[j] ^ cw[j]; + } + storeCodeword(ctx->helperData, i * WC_PUF_BCH_N, helperCw); + } + } + + /* compute identity = SHA-256(stableBits) */ + ret = wc_PufHashDirect(ctx->stableBits, WC_PUF_STABLE_BYTES, ctx->identity); + if (ret != 0) + return PUF_ENROLL_E; + + ctx->flags |= WC_PUF_FLAG_ENROLLED | WC_PUF_FLAG_READY; + return 0; +} + +int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz) +{ + int i, ret; + + if (ctx == NULL || helperData == NULL) + return BAD_FUNC_ARG; + if (helperSz < WC_PUF_HELPER_BYTES) + return PUF_RECONSTRUCT_E; + + XMEMSET(ctx->stableBits, 0, WC_PUF_STABLE_BYTES); + + for (i = 0; i < WC_PUF_NUM_CODEWORDS; i++) { + byte rawCw[16]; + byte helperCw[16]; + byte noisyCw[16]; + byte msg[8]; + int bitOff = i * 128; + int j; + + /* get raw SRAM bits for this codeword */ + 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)); + } + + /* noisy codeword = raw XOR helper */ + for (j = 0; j < 16; j++) { + noisyCw[j] = rawCw[j] ^ helperCw[j]; + } + + /* BCH decode to recover original message */ + ret = bch_decode(noisyCw, msg); + if (ret != 0) + return PUF_RECONSTRUCT_E; + + XMEMCPY(ctx->stableBits + i * 8, msg, 8); + } + + /* compute identity */ + ret = wc_PufHashDirect(ctx->stableBits, WC_PUF_STABLE_BYTES, ctx->identity); + if (ret != 0) + return PUF_RECONSTRUCT_E; + + ctx->flags |= WC_PUF_FLAG_READY; + return 0; +} + +int wc_PufDeriveKey(wc_PufCtx* ctx, const byte* info, word32 infoSz, + byte* key, word32 keySz) +{ + if (ctx == NULL || key == NULL) + return BAD_FUNC_ARG; + if (!(ctx->flags & WC_PUF_FLAG_READY)) + return PUF_DERIVE_KEY_E; + if (keySz == 0) + return BAD_FUNC_ARG; + +#ifdef HAVE_HKDF + { + /* HKDF with stable bits as IKM, identity as salt */ + int ret; + ret = wc_HKDF(WC_PUF_HASH_TYPE, + ctx->stableBits, WC_PUF_STABLE_BYTES, + ctx->identity, WC_PUF_ID_SZ, + info, infoSz, + key, keySz); + if (ret != 0) + return PUF_DERIVE_KEY_E; + + return 0; + } +#else + (void)info; + (void)infoSz; + return PUF_DERIVE_KEY_E; +#endif +} + +int wc_PufGetIdentity(wc_PufCtx* ctx, byte* id, word32 idSz) +{ + if (ctx == NULL || id == NULL) + return BAD_FUNC_ARG; + if (!(ctx->flags & WC_PUF_FLAG_READY)) + return PUF_IDENTITY_E; + if (idSz < WC_PUF_ID_SZ) + return PUF_IDENTITY_E; + + XMEMCPY(id, ctx->identity, WC_PUF_ID_SZ); + return 0; +} + +int wc_PufZeroize(wc_PufCtx* ctx) +{ + if (ctx == NULL) + return BAD_FUNC_ARG; + + ForceZero(ctx, sizeof(wc_PufCtx)); + return 0; +} + +#ifdef WOLFSSL_PUF_TEST +int wc_PufSetTestData(wc_PufCtx* ctx, const byte* data, word32 sz) +{ + if (ctx == NULL || data == NULL) + return BAD_FUNC_ARG; + if (sz < WC_PUF_RAW_BYTES) + return PUF_READ_E; + + /* Copy test data directly into rawSram and set flag */ + XMEMCPY(ctx->rawSram, data, WC_PUF_RAW_BYTES); + ctx->testDataSet = 1; + return 0; +} +#endif /* WOLFSSL_PUF_TEST */ + +#endif /* !HAVE_FIPS */ +#endif /* WOLFSSL_PUF */ diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index a3d6dcfcda3..d4a19a502f6 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -434,6 +434,9 @@ static const byte const_byte_array[] = "A+Gd\0\0\0"; #ifdef WOLFSSL_SM4 #include #endif +#ifdef WOLFSSL_PUF + #include +#endif #ifdef HAVE_LIBZ #include #endif @@ -697,6 +700,9 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t camellia_test(void); #ifdef WOLFSSL_SM4 WOLFSSL_TEST_SUBROUTINE wc_test_ret_t sm4_test(void); #endif +#ifdef WOLFSSL_PUF +WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void); +#endif #ifdef WC_RSA_NO_PADDING WOLFSSL_TEST_SUBROUTINE wc_test_ret_t rsa_no_pad_test(void); #endif @@ -2624,6 +2630,13 @@ options: [-s max_relative_stack_bytes] [-m max_relative_heap_memory_bytes]\n\ TEST_PASS("SM-4 test passed!\n"); #endif +#ifdef WOLFSSL_PUF + if ( (ret = puf_test()) != 0) + return err_sys("PUF test failed!\n", ret); + else + TEST_PASS("PUF test passed!\n"); +#endif + #if !defined(NO_RSA) && !defined(HAVE_RENESAS_SYNC) #ifdef WC_RSA_NO_PADDING if ( (ret = rsa_no_pad_test()) != 0) @@ -20158,6 +20171,190 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t sm4_test(void) } #endif +#ifdef WOLFSSL_PUF +WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) +{ +#if defined(WOLFSSL_PUF_TEST) && defined(HAVE_HKDF) && !defined(NO_SHA256) + wc_test_ret_t ret = 0; + wc_PufCtx ctx; + byte key1[WC_PUF_KEY_SZ]; + byte key2[WC_PUF_KEY_SZ]; + byte id1[WC_PUF_ID_SZ]; + byte id2[WC_PUF_ID_SZ]; + + /* 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 + }; + + /* noisy SRAM: same as testSram but with a few flipped bits */ + byte noisySram[WC_PUF_RAW_BYTES]; + byte helperBuf[WC_PUF_HELPER_BYTES]; + const byte info[] = "puf-test-context"; + + WOLFSSL_ENTER("puf_test"); + + /* ---- Test 1: Init ---- */ + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + /* ---- Test 2: SetTestData + Enroll ---- */ + ret = wc_PufSetTestData(&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); + + /* save helper data and identity */ + XMEMCPY(helperBuf, ctx.helperData, WC_PUF_HELPER_BYTES); + + ret = wc_PufGetIdentity(&ctx, id1, sizeof(id1)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + /* derive a key */ + ret = wc_PufDeriveKey(&ctx, info, sizeof(info), key1, sizeof(key1)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + /* ---- Test 3: Reconstruct with same data (no noise) ---- */ + 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); + + /* need to call ReadSram to populate rawSram (test data already set) */ + ret = wc_PufReadSram(&ctx, testSram, sizeof(testSram)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + ret = wc_PufReconstruct(&ctx, helperBuf, WC_PUF_HELPER_BYTES); + 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); + + /* identity must match */ + if (XMEMCMP(id1, id2, WC_PUF_ID_SZ) != 0) + return WC_TEST_RET_ENC_NC; + + /* derive key again - must match */ + ret = wc_PufDeriveKey(&ctx, info, sizeof(info), key2, sizeof(key2)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + if (XMEMCMP(key1, key2, WC_PUF_KEY_SZ) != 0) + return WC_TEST_RET_ENC_NC; + + /* ---- Test 4: Reconstruct with noisy data (few bit flips) ---- */ + 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 */ + + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + ret = wc_PufSetTestData(&ctx, noisySram, sizeof(noisySram)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + ret = wc_PufReadSram(&ctx, noisySram, sizeof(noisySram)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + ret = wc_PufReconstruct(&ctx, helperBuf, WC_PUF_HELPER_BYTES); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + /* identity should still match after error correction */ + 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; + + /* derived key should still match */ + ret = wc_PufDeriveKey(&ctx, info, sizeof(info), key2, sizeof(key2)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + if (XMEMCMP(key1, key2, WC_PUF_KEY_SZ) != 0) + return WC_TEST_RET_ENC_NC; + + /* ---- Test 5: Bad argument checks ---- */ + if (wc_PufInit(NULL) != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + return WC_TEST_RET_ENC_NC; + + if (wc_PufReadSram(NULL, testSram, sizeof(testSram)) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + return WC_TEST_RET_ENC_NC; + + if (wc_PufDeriveKey(&ctx, info, sizeof(info), NULL, 32) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + return WC_TEST_RET_ENC_NC; + + /* ---- Test 6: Zeroize ---- */ + ret = wc_PufZeroize(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + /* after zeroize, derive should fail (not ready) */ + if (wc_PufDeriveKey(&ctx, info, sizeof(info), key1, sizeof(key1)) + != WC_NO_ERR_TRACE(PUF_DERIVE_KEY_E)) + return WC_TEST_RET_ENC_NC; + + return 0; +#else + return 0; +#endif /* WOLFSSL_PUF_TEST && HAVE_HKDF && !NO_SHA256 */ +} +#endif /* WOLFSSL_PUF */ + #ifdef HAVE_XCHACHA WOLFSSL_TEST_SUBROUTINE wc_test_ret_t XChaCha_test(void) { diff --git a/wolfssl/wolfcrypt/error-crypt.h b/wolfssl/wolfcrypt/error-crypt.h index db5530045ac..2c9289e63a7 100644 --- a/wolfssl/wolfcrypt/error-crypt.h +++ b/wolfssl/wolfcrypt/error-crypt.h @@ -313,8 +313,16 @@ enum wolfCrypt_ErrorCodes { ALREADY_E = -1007, /* Operation was redundant or preempted */ SEQ_OVERFLOW_E = -1008, /* Sequence counter would overflow */ - WC_SPAN2_LAST_E = -1008, /* Update to indicate last used error code */ - WC_LAST_E = -1008, /* the last code used either here or in + + PUF_INIT_E = -1009, /* PUF initialization failed */ + PUF_READ_E = -1010, /* PUF SRAM read failed */ + PUF_ENROLL_E = -1011, /* PUF enrollment failed */ + PUF_RECONSTRUCT_E = -1012, /* PUF reconstruction failed */ + PUF_DERIVE_KEY_E = -1013, /* PUF key derivation failed */ + PUF_IDENTITY_E = -1014, /* PUF identity retrieval failed */ + + WC_SPAN2_LAST_E = -1014, /* Update to indicate last used error code */ + WC_LAST_E = -1014, /* the last code used either here or in * error-ssl.h */ WC_SPAN2_MIN_CODE_E = -1999, /* Last usable code in span 2 */ diff --git a/wolfssl/wolfcrypt/puf.h b/wolfssl/wolfcrypt/puf.h new file mode 100644 index 00000000000..fc80c12a9e3 --- /dev/null +++ b/wolfssl/wolfcrypt/puf.h @@ -0,0 +1,108 @@ +/* puf.h + * + * 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 + */ + +/*! + \file wolfssl/wolfcrypt/puf.h + \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. + + Build: ./configure --enable-puf (auto-enables HKDF) + + For a bare-metal example (tested on NUCLEO-H563ZI), see: + https://github.com/wolfSSL/wolfssl-examples/tree/master/puf +*/ + +#ifndef WOLF_CRYPT_PUF_H +#define WOLF_CRYPT_PUF_H + +#include + +#ifdef WOLFSSL_PUF + +#include + +#ifdef __cplusplus + extern "C" { +#endif + +/* BCH(127,64,t=10) parameters */ +#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 */ + +/* 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 */ + +/* Output key size */ +#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 + +/* Flags for wc_PufCtx.flags */ +#define WC_PUF_FLAG_ENROLLED 0x01 +#define WC_PUF_FLAG_READY 0x02 + +typedef struct wc_PufCtx { + byte rawSram[WC_PUF_RAW_BYTES]; /* raw SRAM readout */ + byte helperData[WC_PUF_HELPER_BYTES]; /* enrollment helper data */ + byte stableBits[WC_PUF_STABLE_BYTES]; /* reconstructed stable bits */ + byte identity[WC_PUF_ID_SZ]; /* device identity hash */ + word32 flags; + +#ifdef WOLFSSL_PUF_TEST + word32 testDataSet; /* flag: test data was injected */ +#endif +} wc_PufCtx; + +WOLFSSL_API int wc_PufInit(wc_PufCtx* ctx); +WOLFSSL_API int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, + word32 sramSz); +WOLFSSL_API int wc_PufEnroll(wc_PufCtx* ctx); +WOLFSSL_API int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, + word32 helperSz); +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_PufZeroize(wc_PufCtx* ctx); + +#ifdef WOLFSSL_PUF_TEST +WOLFSSL_API int wc_PufSetTestData(wc_PufCtx* ctx, const byte* data, word32 sz); +#endif + +#ifdef __cplusplus + } /* extern "C" */ +#endif + +#endif /* WOLFSSL_PUF */ + +#endif /* WOLF_CRYPT_PUF_H */