From c5c63c0ba597cff3ad202795ac879b352f5be0f3 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 11 Aug 2026 07:30:09 -0700 Subject: [PATCH] wolfCrypt SRAM PUF: health test the raw readout before accepting it --- .github/workflows/puf.yml | 15 +- CMakeLists.txt | 25 ++ ChangeLog.md | 22 ++ README.md | 4 +- cmake/options.h.in | 4 + doc/dox_comments/header_files-ja/puf.h | 33 ++- doc/dox_comments/header_files/puf.h | 69 +++++- wolfcrypt/src/puf.c | 146 ++++++++++- wolfcrypt/test/test.c | 331 +++++++++++++++++++++++++ wolfssl/wolfcrypt/puf.h | 61 ++++- 10 files changed, 701 insertions(+), 9 deletions(-) diff --git a/.github/workflows/puf.yml b/.github/workflows/puf.yml index bc1fcedef3..71373ed072 100644 --- a/.github/workflows/puf.yml +++ b/.github/workflows/puf.yml @@ -41,6 +41,15 @@ jobs: - "--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" + # Non-default readout health-test band. puf_test() derives the + # expected verdict from the configured band, and two of its bias + # fixtures (~25% and ~72% ones) sit between this band and the + # 35/65 default, so they are rejected there and accepted here: + # the entry exercises the tunable path rather than just building + # it. WOLFSSL_PUF_TEST_BAND_WIDE makes puf_test() pin these exact + # percentages, so a misspelled or mistyped band -D is a build + # failure here instead of a green run against some other band. + - "--enable-puf --enable-puf-test CPPFLAGS=\"-DWC_PUF_HW_MIN_PCT=20 -DWC_PUF_HW_MAX_PCT=80 -DWOLFSSL_PUF_TEST_BAND_WIDE\"" steps: - uses: actions/checkout@v5 name: Checkout wolfSSL @@ -78,12 +87,16 @@ jobs: run: | cmake -S . -B build_puf -DWOLFSSL_PUF=yes \ -DWOLFSSL_PUF_PROFILE=strong \ - -DWOLFSSL_PUF_NUM_CODEWORDS=32 + -DWOLFSSL_PUF_NUM_CODEWORDS=32 \ + -DWOLFSSL_PUF_HW_MIN_PCT=20 \ + -DWOLFSSL_PUF_HW_MAX_PCT=80 - 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 WC_PUF_HW_MIN_PCT 20$' build_puf/wolfssl/options.h + grep -q '^#define WC_PUF_HW_MAX_PCT 80$' build_puf/wolfssl/options.h grep -q '^#define WOLFSSL_PUF$' build_puf/wolfssl/options.h - name: Build diff --git a/CMakeLists.txt b/CMakeLists.txt index bb2b584e69..f35291e03d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2352,6 +2352,14 @@ if(WOLFSSL_PUF) set(WOLFSSL_PUF_NUM_CODEWORDS "" CACHE STRING "Number of PUF BCH codewords (1-4095, default: 16)") + # Readout health-test band, same cache-name reasoning as above. An + # application that measures a region with wc_PufCheckSram needs the same + # band as the library, so route it through options.h rather than CFLAGS. + set(WOLFSSL_PUF_HW_MIN_PCT "" CACHE STRING + "PUF readout minimum Hamming weight, percent (1-98, default: 35)") + set(WOLFSSL_PUF_HW_MAX_PCT "" CACHE STRING + "PUF readout maximum Hamming weight, percent (2-99, default: 65)") + list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_PUF" "-DWOLFSSL_PUF_SRAM" @@ -2366,6 +2374,23 @@ if(WOLFSSL_PUF) list(APPEND WOLFSSL_DEFINITIONS "-DWC_PUF_NUM_CODEWORDS=${WOLFSSL_PUF_NUM_CODEWORDS}") endif() + foreach(pufBand MIN MAX) + if(WOLFSSL_PUF_HW_${pufBand}_PCT) + if(NOT WOLFSSL_PUF_HW_${pufBand}_PCT MATCHES "^[0-9]+$" OR + WOLFSSL_PUF_HW_${pufBand}_PCT LESS 1 OR + WOLFSSL_PUF_HW_${pufBand}_PCT GREATER 99) + message(FATAL_ERROR + "WOLFSSL_PUF_HW_${pufBand}_PCT must be an integer from 1 to 99") + endif() + list(APPEND WOLFSSL_DEFINITIONS + "-DWC_PUF_HW_${pufBand}_PCT=${WOLFSSL_PUF_HW_${pufBand}_PCT}") + endif() + endforeach() + if(WOLFSSL_PUF_HW_MIN_PCT AND WOLFSSL_PUF_HW_MAX_PCT AND + NOT WOLFSSL_PUF_HW_MIN_PCT LESS WOLFSSL_PUF_HW_MAX_PCT) + message(FATAL_ERROR + "WOLFSSL_PUF_HW_MIN_PCT must be less than WOLFSSL_PUF_HW_MAX_PCT") + endif() if(WOLFSSL_PUF_PROFILE STREQUAL "small") list(APPEND WOLFSSL_DEFINITIONS "-DWC_PUF_BCH_T=7") elseif(WOLFSSL_PUF_PROFILE STREQUAL "strong") diff --git a/ChangeLog.md b/ChangeLog.md index 55652f4971..2c74b06564 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,6 +2,28 @@ ## Behavioral Changes +* **Behavioral change (`wc_PufReadSram` health tests the raw SRAM readout)**: + the raw readout is now health tested before the context accepts it, and a + readout that cannot be SRAM power-on noise is rejected with `PUF_READ_E` + instead of deriving a key from it. Rejected are a 128-bit block that is all + zero or all ones, a block that repeats the one before it, and a total + Hamming weight outside `WC_PUF_HW_MIN_PCT`..`WC_PUF_HW_MAX_PCT` (35% to 65% + by default). A rejected readout leaves the context unusable, so + `wc_PufEnroll()` and `wc_PufReconstruct()` refuse to run on it, while output + already derived from a readout that did pass stays available. This turns a + previously silent failure - a degenerate region derives a key that is + identical on every device and computable offline - into a visible one. It + catches degenerate shapes only: ordinary firmware content left in the region + is device-identical yet neither constant nor strongly biased, so sampling + from reset, before .bss/.data init, remains a requirement rather than + something the test can enforce. An integration whose silicon is strongly + biased, or which was sampling its region too late, will now see `PUF_READ_E` + where it previously saw success. Measure a candidate region with the new + `wc_PufCheckSram()` and widen the band with `WC_PUF_HW_MIN_PCT` / + `WC_PUF_HW_MAX_PCT` if the silicon warrants it. The helper-data format and + the derived key are unchanged, so helper data enrolled by wolfSSL 5.9.2 + stays valid. + * **Behavioral change (`wolfSSL_shutdown` when no close_notify can be sent)**: when the connection is already closed or reset and no close_notify was ever sent, the shutdown exchange can never complete. That case now returns diff --git a/README.md b/README.md index 01e634462c..48e45d3853 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ or contact fips@wolfssl.com. wolfCrypt also includes support for deriving device-unique keys from hardware entropy (`--enable-puf[=small|balanced|strong|strongest]`, selecting the BCH error-correction -strength). An example exists at +strength). Each raw SRAM readout is health tested before use, so a degenerate readout - +all zero, all ones, a repeating block, or an implausible bit bias - cannot silently +produce a device-independent key. An example exists at [SRAM PUF](https://github.com/wolfSSL/wolfssl-examples/tree/master/puf). ## Why Choose wolfSSL? diff --git a/cmake/options.h.in b/cmake/options.h.in index ac9bc57f4d..c7ecfd7c4c 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -137,6 +137,10 @@ extern "C" { #cmakedefine WC_PUF_BCH_T @WC_PUF_BCH_T@ #undef WC_PUF_NUM_CODEWORDS #cmakedefine WC_PUF_NUM_CODEWORDS @WC_PUF_NUM_CODEWORDS@ +#undef WC_PUF_HW_MIN_PCT +#cmakedefine WC_PUF_HW_MIN_PCT @WC_PUF_HW_MIN_PCT@ +#undef WC_PUF_HW_MAX_PCT +#cmakedefine WC_PUF_HW_MAX_PCT @WC_PUF_HW_MAX_PCT@ #undef HAVE_HPKE #cmakedefine HAVE_HPKE #undef HAVE_KEYING_MATERIAL diff --git a/doc/dox_comments/header_files-ja/puf.h b/doc/dox_comments/header_files-ja/puf.h index 1960432f64..ec2e115db3 100644 --- a/doc/dox_comments/header_files-ja/puf.h +++ b/doc/dox_comments/header_files-ja/puf.h @@ -32,9 +32,11 @@ int wc_PufInit(wc_PufCtx* ctx); \brief 生のSRAMデータをPUFコンテキストに読み込みます。電源投入時の状態を保持するため、sramAddrはNOLOADリンカセクションを指している必要があります。 + 読み込まれたデータは、受け入れられる前にwc_PufCheckSram()によるヘルスチェックを受けます。退化したデータ(典型例は.bss初期化でクリアされた領域。Cランタイム起動後にサンプリングしてしまう、よくある立ち上げ時の誤り)はPUF_READ_Eで拒否され、コンテキストはwc_PufEnroll()およびwc_PufReconstruct()から使用できない状態のままになります。なお、このチェックが検出するのは退化したパターンであり、書き込み済みの領域すべてではありません。リセット直後にサンプリングするという要件は引き続き必要です。 + \return 0 成功した場合に返されます \return BAD_FUNC_ARG ctxまたはsramAddrがNULLの場合に返されます - \return PUF_READ_E sramSzがWC_PUF_RAW_BYTES未満の場合に返されます + \return PUF_READ_E sramSzがWC_PUF_RAW_BYTES未満の場合、または読み込まれたデータがヘルスチェックに失敗した場合に返されます \param ctx wc_PufCtx構造体へのポインタ \param sramAddr 生のSRAMメモリ領域へのポインタ @@ -48,11 +50,40 @@ int wc_PufInit(wc_PufCtx* ctx); \endcode \sa wc_PufInit + \sa wc_PufCheckSram \sa wc_PufEnroll \sa wc_PufReconstruct */ int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, word32 sramSz); +/*! + \ingroup PUF + + \brief 候補となる生のSRAMデータを、コンテキストに読み込むことなくヘルスチェックします。SRAMの電源投入時ノイズとしてありえないデータ、すなわち、すべて0またはすべて1の128ビットブロック、直前のブロックと同一のブロック、あるいは全体のハミング重みがWC_PUF_HW_MIN_PCTからWC_PUF_HW_MAX_PCTの範囲(既定ではWC_PUF_RAW_BITSの35%から65%)を外れるデータを拒否します。 + + wc_PufReadSram()はすべての読み込みに対してこのチェックを適用するため、本関数を直接呼び出す必要があるのは、基板の立ち上げ時に候補となるSRAM領域を評価する場合、または読み込みが拒否された理由を報告する場合のみです。onesCountは省略可能で、サイズチェックを通過した場合は、その後データが拒否された場合でも書き込まれるため、拒否された領域の偏りを測定値として取得できます。sramAddrがNULLの場合、またはsramSzが不足している場合は書き込まれません。 + + \return 0 PUF材料として妥当なデータである場合に返されます + \return BAD_FUNC_ARG sramAddrがNULLの場合に返されます + \return PUF_READ_E sramSzがWC_PUF_RAW_BYTES未満の場合、またはいずれかのチェックに失敗した場合に返されます + + \param sramAddr 生のSRAMメモリ領域へのポインタ + \param sramSz SRAMバッファのサイズ(WC_PUF_RAW_BYTES以上でなければなりません) + \param onesCount 省略可能。読み込まれたデータの先頭WC_PUF_RAW_BYTES中の1ビットの個数(WC_PUF_RAW_BITS中)を受け取ります。sramAddrがNULLでなくsramSzが十分な大きさであれば、判定結果にかかわらず書き込まれます。これは生のPUF材料の性質を表す値のため、立ち上げ時の測定用途にとどめ、製品ファームウェアから出力しないでください + + _Example_ + \code + word32 ones = 0; + ret = wc_PufCheckSram((const byte*)puf_sram, sizeof(puf_sram), &ones); + printf("PUF SRAM bias %u/%u ones, ret %d\n", + (unsigned)ones, (unsigned)WC_PUF_RAW_BITS, ret); + \endcode + + \sa wc_PufReadSram + \sa wc_PufEnroll +*/ +int wc_PufCheckSram(const byte* sramAddr, word32 sramSz, word32* onesCount); + /*! \ingroup PUF diff --git a/doc/dox_comments/header_files/puf.h b/doc/dox_comments/header_files/puf.h index 487870884f..397483ab78 100644 --- a/doc/dox_comments/header_files/puf.h +++ b/doc/dox_comments/header_files/puf.h @@ -17,6 +17,21 @@ the helper data at enrollment and pass it to wc_PufReconstructEx, or compare it against wc_PufGetProfileId(), to catch a build mismatch. + Every readout handed to wc_PufReadSram() is health tested first (see + wc_PufCheckSram). A degenerate readout - all zero, all ones, or a repeating + block - would otherwise pass cleanly through encoding, masking, decoding + and HKDF and derive a key that is the same on every device. + WC_PUF_HW_MIN_PCT and WC_PUF_HW_MAX_PCT (default 35 and 65) set the + Hamming-weight band the readout must fall inside. + + The test rejects degenerate readouts, not every already-written region: + ordinary firmware content - .data copied from flash, a string table, a + previous boot stage - is identical on every device yet neither constant nor + strongly biased, so it can pass. Sampling the region from reset, before + .bss/.data initialization and before it is used as stack or heap, remains a + requirement of correct NOLOAD placement rather than something this test can + enforce. + For a complete bare-metal example (tested on NUCLEO-H563ZI), see https://github.com/wolfSSL/wolfssl-examples/tree/master/puf */ @@ -52,9 +67,18 @@ int wc_PufInit(wc_PufCtx* ctx); required size, WC_PUF_RAW_BYTES, scales with WC_PUF_NUM_CODEWORDS (256 bytes at the default 16 codewords). + The readout is health tested with wc_PufCheckSram() before it is accepted. + A degenerate readout - typically a region already cleared by .bss init, the + common bring-up mistake of sampling after C runtime startup - is rejected + with PUF_READ_E, and the context is left unusable by wc_PufEnroll() and + wc_PufReconstruct(). The test catches degenerate shapes, not every + already-written region, so it does not remove the requirement to sample + from reset. + \return 0 on success \return BAD_FUNC_ARG if ctx or sramAddr is NULL - \return PUF_READ_E if sramSz < WC_PUF_RAW_BYTES + \return PUF_READ_E if sramSz < WC_PUF_RAW_BYTES, or if the readout fails + the health test \param ctx pointer to wc_PufCtx structure \param sramAddr pointer to raw SRAM memory region @@ -68,11 +92,54 @@ int wc_PufInit(wc_PufCtx* ctx); \endcode \sa wc_PufInit + \sa wc_PufCheckSram \sa wc_PufEnroll \sa wc_PufReconstruct */ int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, word32 sramSz); +/*! + \ingroup PUF + + \brief Health test a candidate raw SRAM readout without loading it into a + context. Rejects a readout that cannot be SRAM power-on noise: any 128-bit + block that is all zero or all ones, any block that repeats the block before + it, or a total Hamming weight outside the WC_PUF_HW_MIN_PCT to + WC_PUF_HW_MAX_PCT band (default 35% to 65% of WC_PUF_RAW_BITS). + + wc_PufReadSram() applies this test to every readout, so calling it directly + is only needed to qualify a candidate SRAM region during board bring-up, or + to report why a read was refused. onesCount is optional and is written + whenever the size check passes - including when the readout is then + rejected, so the measured bias of a rejected region is still available. It + is left untouched when sramAddr is NULL or sramSz is short. + + \return 0 if the readout is plausible PUF material + \return BAD_FUNC_ARG if sramAddr is NULL + \return PUF_READ_E if sramSz < WC_PUF_RAW_BYTES, or if the readout fails + any of the checks + + \param sramAddr pointer to raw SRAM memory region + \param sramSz size of SRAM buffer (must be >= WC_PUF_RAW_BYTES) + \param onesCount optional; receives the number of one bits in the first + WC_PUF_RAW_BYTES of the readout, out of WC_PUF_RAW_BITS. Written whenever + sramAddr is non-NULL and sramSz is large enough, whatever the verdict. It + is a property of the raw PUF material, so treat it as a bring-up + measurement and do not report it from production firmware + + _Example_ + \code + word32 ones = 0; + ret = wc_PufCheckSram((const byte*)puf_sram, sizeof(puf_sram), &ones); + printf("PUF SRAM bias %u/%u ones, ret %d\n", + (unsigned)ones, (unsigned)WC_PUF_RAW_BITS, ret); + \endcode + + \sa wc_PufReadSram + \sa wc_PufEnroll +*/ +int wc_PufCheckSram(const byte* sramAddr, word32 sramSz, word32* onesCount); + /*! \ingroup PUF diff --git a/wolfcrypt/src/puf.c b/wolfcrypt/src/puf.c index 9afc5974f5..26ae14bc0c 100644 --- a/wolfcrypt/src/puf.c +++ b/wolfcrypt/src/puf.c @@ -510,6 +510,107 @@ static void storeCodeword(byte* helper, int bitOffset, const byte* cw) } +/* Population count. Nibble table, not a builtin: bare-metal C89 toolchains. */ +static word32 pufBitCount(const byte* buf, word32 sz) +{ + static const byte nibbleBits[16] = + { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 }; + word32 i; + word32 count = 0; + + for (i = 0; i < sz; i++) { + count += nibbleBits[buf[i] & 0x0F]; + count += nibbleBits[(buf[i] >> 4) & 0x0F]; + } + + return count; +} + +/* Startup health test on a candidate raw SRAM readout. Constant input is + * self-consistent through encoding, masking, decoding and HKDF, so a + * degenerate readout would yield a key identical on every device. Reject it + * here, at the only production boundary raw PUF material enters through + * (wc_PufSetTestData deliberately bypasses it under WOLFSSL_PUF_TEST, so the + * test vectors it injects are not filtered). + * + * Three O(WC_PUF_RAW_BYTES) checks: no block all zero or all ones, no block + * repeating the one before it, and total Hamming weight inside the + * WC_PUF_HW_MIN_PCT..WC_PUF_HW_MAX_PCT band. The first two false-reject real + * SRAM with probability ~2^-127 per block; the band passes biased silicon. + * + * onesCount is optional. It is written whenever the size check passes - + * including when the readout is then rejected, so bring-up code can report the + * measured bias of a region it just had refused - and left untouched when + * sramAddr is NULL or sramSz is short, because there is nothing to measure. */ +int wc_PufCheckSram(const byte* sramAddr, word32 sramSz, word32* onesCount) +{ + const byte* slice; + word32 ones; + word32 i; + int j; + int allZero; + int allOnes; + + WOLFSSL_ENTER("wc_PufCheckSram"); + + if (sramAddr == NULL) + return BAD_FUNC_ARG; + if (sramSz < WC_PUF_RAW_BYTES) { + /* every other PUF_READ_E from here names its cause, and a region + * sized for the wrong WC_PUF_NUM_CODEWORDS is the likeliest one */ + WOLFSSL_MSG("PUF: SRAM readout smaller than WC_PUF_RAW_BYTES"); + return PUF_READ_E; + } + + ones = pufBitCount(sramAddr, WC_PUF_RAW_BYTES); + if (onesCount != NULL) + *onesCount = ones; + + /* one codeword per stride, pinned by the guard in puf.h, so a stride is + * one block here */ + for (i = 0; i < (word32)WC_PUF_NUM_CODEWORDS; i++) { + slice = sramAddr + (i * WC_PUF_RAW_STRIDE_BYTES); + + allZero = 1; + allOnes = 1; + for (j = 0; j < WC_PUF_RAW_STRIDE_BYTES; j++) { + if (slice[j] != 0x00) + allZero = 0; + if (slice[j] != 0xFF) + allOnes = 0; + } + if (allZero) { + WOLFSSL_MSG("PUF: all-zero block in SRAM readout"); + return PUF_READ_E; + } + if (allOnes) { + WOLFSSL_MSG("PUF: all-ones block in SRAM readout"); + return PUF_READ_E; + } + + /* Previous block only: that covers what this test exists for - a + * uniform fill, a memset, a re-read of one address. Longer-period + * structure is not chased; a cheap startup test cannot tell it from + * noise. */ + if (i > 0 && XMEMCMP(slice, slice - WC_PUF_RAW_STRIDE_BYTES, + WC_PUF_RAW_STRIDE_BYTES) == 0) { + WOLFSSL_MSG("PUF: repeated block in SRAM readout"); + return PUF_READ_E; + } + } + + if (ones * 100U < (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MIN_PCT) { + WOLFSSL_MSG("PUF: SRAM readout has too few one bits"); + return PUF_READ_E; + } + if (ones * 100U > (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MAX_PCT) { + WOLFSSL_MSG("PUF: SRAM readout has too many one bits"); + return PUF_READ_E; + } + + return 0; +} + int wc_PufInit(wc_PufCtx* ctx) { WOLFSSL_ENTER("wc_PufInit"); @@ -524,12 +625,28 @@ int wc_PufInit(wc_PufCtx* ctx) int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, word32 sramSz) { + int ret; + WOLFSSL_ENTER("wc_PufReadSram"); - if (ctx == NULL || sramAddr == NULL) + if (ctx == NULL) return BAD_FUNC_ARG; - if (sramSz < WC_PUF_RAW_BYTES) + + /* Any failed read invalidates the readout the context was holding, so the + * flag goes down before every path that can fail, bad arguments included. + * Otherwise a read that returns an error still leaves wc_PufEnroll and + * wc_PufReconstruct running on whatever an earlier call had accepted. */ + ctx->flags &= (word32)~WC_PUF_FLAG_SRAM_SET; + + if (sramAddr == NULL) + return BAD_FUNC_ARG; + + /* must be checked here too: it guards the full-size XMEMCPY below, and + * this is the path an integrator hits, so it names its cause as well */ + if (sramSz < WC_PUF_RAW_BYTES) { + WOLFSSL_MSG("PUF: SRAM readout smaller than WC_PUF_RAW_BYTES"); return PUF_READ_E; + } #ifdef WOLFSSL_PUF_TEST if (ctx->testDataSet) { @@ -539,7 +656,27 @@ int wc_PufReadSram(wc_PufCtx* ctx, const byte* sramAddr, word32 sramSz) } #endif + /* Health test the copy that will actually be used, not the caller's + * buffer: the region is volatile by construction, so the bytes read for + * the copy are not guaranteed to be the bytes the test saw. + * + * WC_PUF_FLAG_SRAM_SET is already down (above), so the context does not + * look like it holds a validated readout for the window in which rawSram + * holds unvalidated bytes; it goes back up only once the test passes. A + * rejected readout is scrubbed rather than left resident, and wc_PufEnroll + * and wc_PufReconstruct then refuse to run on the context. Output already + * derived from a readout that did pass - identity, helper data, a derived + * key - is unaffected and stays available. */ XMEMCPY(ctx->rawSram, sramAddr, WC_PUF_RAW_BYTES); + ret = wc_PufCheckSram(ctx->rawSram, WC_PUF_RAW_BYTES, NULL); + if (ret != 0) { + /* only this path scrubs: it is the one that copied unvalidated bytes + * in. The paths that fail before the copy leave the previously + * accepted readout in place, unreachable behind the cleared flag. */ + ForceZero(ctx->rawSram, WC_PUF_RAW_BYTES); + return ret; + } + ctx->flags |= WC_PUF_FLAG_SRAM_SET; return 0; } @@ -578,7 +715,8 @@ int wc_PufEnroll(wc_PufCtx* ctx) for (i = 0; i < WC_PUF_NUM_CODEWORDS; i++) { /* extract k message bits from raw SRAM */ - int bitOff = i * 128; /* 128-bit stride per codeword (n=127 fits) */ + /* one codeword per raw stride; see WC_PUF_RAW_STRIDE_BITS in puf.h */ + int bitOff = i * WC_PUF_RAW_STRIDE_BITS; int j; XMEMSET(msg, 0, sizeof(msg)); for (j = 0; j < WC_PUF_BCH_K; j++) { @@ -665,7 +803,7 @@ int wc_PufReconstructEx(wc_PufCtx* ctx, const byte* helperData, XMEMSET(ctx->stableBits, 0, WC_PUF_STABLE_BYTES); for (i = 0; i < WC_PUF_NUM_CODEWORDS; i++) { - int bitOff = i * 128; + int bitOff = i * WC_PUF_RAW_STRIDE_BITS; int j; /* get raw SRAM bits for this codeword */ diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 9206552b7b..0556fd5351 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -25043,6 +25043,27 @@ static void puf_fill_sram(byte* sram, word32 sz) } } +/* Build a raw SRAM image with exactly 'ones' one bits. The bits are split + * evenly over the blocks and each block starts its run at a different offset, + * so no block is constant and no block repeats the one before it: only the + * Hamming-weight band can reject the result. */ +static void puf_fill_weight(byte* sram, word32 ones) +{ + word32 blk, want, k, pos; + + XMEMSET(sram, 0x00, WC_PUF_RAW_BYTES); + for (blk = 0; blk < (word32)WC_PUF_NUM_CODEWORDS; blk++) { + want = ones / (word32)WC_PUF_NUM_CODEWORDS; + if (blk < (ones % (word32)WC_PUF_NUM_CODEWORDS)) + want++; + for (k = 0; k < want; k++) { + pos = (blk + k) % (WC_PUF_CW_BYTES * 8); + sram[(blk * WC_PUF_CW_BYTES) + (pos / 8)] |= + (byte)(1 << (7 - (pos % 8))); + } + } +} + /* 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. */ @@ -25063,13 +25084,35 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) { #if defined(WOLFSSL_PUF_TEST) && defined(HAVE_HKDF) && \ (!defined(NO_SHA256) || defined(WOLFSSL_SHA3)) +/* Set by the CI entry that widens the health-test band. It asserts the band + * really is wider than the default, so a -D that never reached the compiler (a + * typo in either macro name) is a build failure here rather than a job that + * silently retests the default band. */ +#ifdef WOLFSSL_PUF_TEST_BAND_WIDE + /* The .github/workflows/puf.yml entry that sets this also passes + * -DWC_PUF_HW_MIN_PCT=20 -DWC_PUF_HW_MAX_PCT=80. Pin those exact values: + * a misspelled or mistyped band -D then fails the build instead of + * quietly retesting whatever band did reach the compiler. */ + #if WC_PUF_HW_MIN_PCT != 20 || WC_PUF_HW_MAX_PCT != 80 + #error "WOLFSSL_PUF_TEST_BAND_WIDE set, 20/80 band did not arrive" + #endif +#endif 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]; + byte id3[WC_PUF_ID_SZ]; int block, nblocks; + int cw, cwByte; + int bias, inBand; + word32 ones, onesLow, onesHigh; + /* Test 10's four bias fixtures, by fill byte. 0xFE (~82% ones), 0x01 (~13%) + * sit outside any legal band; 0xFC (~72%) and 0x03 (~25%) sit between the + * 35..65 default band and a widened one, so the verdict on them depends on + * how the band was configured. */ + static const byte biasFill[4] = { 0xFE, 0x01, 0xFC, 0x03 }; /* deterministic test SRAM, sized to the selected profile. These buffers * scale with WC_PUF_NUM_CODEWORDS, so keep them off the stack under @@ -25078,6 +25121,8 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) /* noisy SRAM: same as testSram but with a few flipped bits */ WOLFSSL_SMALL_STACK_STATIC byte noisySram[WC_PUF_RAW_BYTES]; WOLFSSL_SMALL_STACK_STATIC byte helperBuf[WC_PUF_HELPER_BYTES]; + /* rewritten per case by Test 10 */ + WOLFSSL_SMALL_STACK_STATIC byte badSram[WC_PUF_RAW_BYTES]; const byte info[] = "puf-test-context"; WOLFSSL_ENTER("puf_test"); @@ -25380,6 +25425,292 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t puf_test(void) return WC_TEST_RET_ENC_NC; } + /* ---- Test 10: raw readout health test ---- * + * Each buffer trips exactly one check. None of these may call + * wc_PufSetTestData first: that sets ctx.testDataSet, which + * short-circuits wc_PufReadSram before the health test can run. */ + + /* all zero: the "SRAM already cleared by .bss init" mistake */ + XMEMSET(badSram, 0x00, sizeof(badSram)); + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (wc_PufReadSram(&ctx, badSram, sizeof(badSram)) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + /* refused, so the context must be usable by neither half of the contract: + * helperBuf still holds the valid Test 2 helper data, and reconstruct + * reaches its WC_PUF_FLAG_SRAM_SET guard after the profile and length + * guards, so both arms need pinning */ + if (wc_PufEnroll(&ctx) != WC_NO_ERR_TRACE(PUF_ENROLL_E)) + return WC_TEST_RET_ENC_NC; + if (wc_PufReconstruct(&ctx, helperBuf, WC_PUF_HELPER_BYTES) + != WC_NO_ERR_TRACE(PUF_RECONSTRUCT_E)) + return WC_TEST_RET_ENC_NC; + /* the measurement is reported once the size check has passed, even when + * the health test then rejects the readout */ + ones = 0xFFFFFFFFU; + if (wc_PufCheckSram(badSram, sizeof(badSram), &ones) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + if (ones != 0) + return WC_TEST_RET_ENC_NC; + + /* all ones */ + XMEMSET(badSram, 0xFF, sizeof(badSram)); + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (wc_PufReadSram(&ctx, badSram, sizeof(badSram)) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + +#if WC_PUF_NUM_CODEWORDS > 1 + /* every block identical, but neither constant nor biased: only the + * repeated-block check can reject this */ + for (cw = 0; cw < WC_PUF_NUM_CODEWORDS; cw++) { + XMEMCPY(badSram + (cw * WC_PUF_CW_BYTES), testSram, WC_PUF_CW_BYTES); + } + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (wc_PufReadSram(&ctx, badSram, sizeof(badSram)) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; +#endif + +#if WC_PUF_NUM_CODEWORDS > 1 + /* The block checks must reach every block, not just the first. Repeating + * the previous block into the *last* one changes the total weight by at + * most a bit, so the repeat check at the end of the loop is what rejects + * this, not the band. */ + puf_fill_weight(badSram, (((word32)WC_PUF_RAW_BITS * WC_PUF_HW_MIN_PCT + + (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MAX_PCT) + / 200U)); + XMEMCPY(badSram + ((WC_PUF_NUM_CODEWORDS - 1) * WC_PUF_CW_BYTES), + badSram + ((WC_PUF_NUM_CODEWORDS - 2) * WC_PUF_CW_BYTES), + WC_PUF_CW_BYTES); + if (wc_PufCheckSram(badSram, WC_PUF_RAW_BYTES, NULL) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; +#endif + +#if WC_PUF_NUM_CODEWORDS > 2 + /* and a constant block in the middle of the buffer */ + puf_fill_weight(badSram, (((word32)WC_PUF_RAW_BITS * WC_PUF_HW_MIN_PCT + + (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MAX_PCT) + / 200U)); + XMEMSET(badSram + WC_PUF_CW_BYTES, 0x00, WC_PUF_CW_BYTES); + if (wc_PufCheckSram(badSram, WC_PUF_RAW_BYTES, NULL) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; +#endif + + /* Hamming-weight band, over the biasFill fixtures. The last byte of each + * block keeps the blocks distinct, so neither block check can fire first + * and only the band decides. The expected verdict is derived from the + * configured band rather than fixed, so a build that widens the band + * reaches a different outcome here instead of retesting the default one: + * 0xFC and 0x03 land between the 35..65 default and a 20..80 band. */ + for (bias = 0; bias < (int)(sizeof(biasFill) / sizeof(biasFill[0])); + bias++) { + for (cw = 0; cw < WC_PUF_NUM_CODEWORDS; cw++) { + for (cwByte = 0; cwByte < WC_PUF_CW_BYTES; cwByte++) { + badSram[(cw * WC_PUF_CW_BYTES) + cwByte] = biasFill[bias]; + } + badSram[(cw * WC_PUF_CW_BYTES) + WC_PUF_CW_BYTES - 1] = (byte)cw; + } + + ones = 0; + ret = wc_PufCheckSram(badSram, sizeof(badSram), &ones); + inBand = (ones * 100U >= (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MIN_PCT && + ones * 100U <= (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MAX_PCT); + if (ret != (inBand ? 0 : WC_NO_ERR_TRACE(PUF_READ_E))) + return WC_TEST_RET_ENC_NC; + + /* the read path must reach the same verdict, and only a readout the + * band accepted may go on to enroll */ + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + ret = wc_PufReadSram(&ctx, badSram, sizeof(badSram)); + if (ret != (inBand ? 0 : WC_NO_ERR_TRACE(PUF_READ_E))) + return WC_TEST_RET_ENC_NC; + + if (inBand) { + ret = wc_PufEnroll(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + } + else if (wc_PufEnroll(&ctx) != WC_NO_ERR_TRACE(PUF_ENROLL_E)) { + return WC_TEST_RET_ENC_NC; + } + } + + /* Band edges, with the verdicts hard-coded rather than derived from the + * band expression: the largest weight inside the band must be accepted and + * one bit more rejected, and likewise at the low edge, so the inclusive + * direction of both comparisons is pinned independently of how + * wc_PufCheckSram spells them. */ + onesLow = (((word32)WC_PUF_RAW_BITS * WC_PUF_HW_MIN_PCT) + 99U) / 100U; + onesHigh = ((word32)WC_PUF_RAW_BITS * WC_PUF_HW_MAX_PCT) / 100U; + + puf_fill_weight(badSram, onesLow); + ones = 0; + ret = wc_PufCheckSram(badSram, WC_PUF_RAW_BYTES, &ones); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (ones != onesLow) + return WC_TEST_RET_ENC_NC; + + puf_fill_weight(badSram, onesLow - 1); + if (wc_PufCheckSram(badSram, WC_PUF_RAW_BYTES, NULL) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + + puf_fill_weight(badSram, onesHigh); + ones = 0; + ret = wc_PufCheckSram(badSram, WC_PUF_RAW_BYTES, &ones); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (ones != onesHigh) + return WC_TEST_RET_ENC_NC; + + puf_fill_weight(badSram, onesHigh + 1); + if (wc_PufCheckSram(badSram, WC_PUF_RAW_BYTES, NULL) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + + /* accessor bad args. onesCount is written only once the size check has + * passed, so both of these must leave the caller's value untouched. */ + ones = 0xFFFFFFFFU; + if (wc_PufCheckSram(NULL, WC_PUF_RAW_BYTES, &ones) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + return WC_TEST_RET_ENC_NC; + if (ones != 0xFFFFFFFFU) + return WC_TEST_RET_ENC_NC; + if (wc_PufCheckSram(testSram, WC_PUF_RAW_BYTES - 1, &ones) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + if (ones != 0xFFFFFFFFU) + return WC_TEST_RET_ENC_NC; + /* and the same two with onesCount omitted */ + if (wc_PufCheckSram(NULL, WC_PUF_RAW_BYTES, NULL) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + return WC_TEST_RET_ENC_NC; + if (wc_PufCheckSram(testSram, WC_PUF_RAW_BYTES - 1, NULL) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + + /* the same guard on the read path: it is all that stands between a short + * caller buffer and the full-size XMEMCPY into ctx->rawSram */ + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (wc_PufReadSram(&ctx, testSram, WC_PUF_RAW_BYTES - 1) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + + /* A plausible readout passes, and its bias is inside the band. testSram + * has a fixed ~50% bias, so a build that deliberately shifts the band off + * 50% rejects it and is behaving correctly; skip the accept path there + * rather than fail. The band-edge cases above cover the accept path for + * any legal band. */ + ones = 0; + ret = wc_PufCheckSram(testSram, sizeof(testSram), &ones); + inBand = (ones * 100U >= (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MIN_PCT && + ones * 100U <= (word32)WC_PUF_RAW_BITS * WC_PUF_HW_MAX_PCT); + /* judged on the measured weight, so a regression in one of the block + * checks fails here instead of silently skipping the accept path */ + if (ret != (inBand ? 0 : WC_NO_ERR_TRACE(PUF_READ_E))) + return WC_TEST_RET_ENC_NC; + + /* A rejected read must invalidate a context that a previous read had + * already validated: the flag is otherwise sticky, and enroll would then + * run on the scrubbed rawSram and derive the same key on every device - + * exactly what the health test exists to prevent. */ + /* seed from a mid-band weight rather than testSram, so the case works + * whatever band the build configured */ + puf_fill_weight(badSram, (onesLow + onesHigh) / 2U); + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + ret = wc_PufReadSram(&ctx, badSram, sizeof(badSram)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + ret = wc_PufEnroll(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + ret = wc_PufGetIdentity(&ctx, id3, sizeof(id3)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + XMEMSET(badSram, 0x00, sizeof(badSram)); + if (wc_PufReadSram(&ctx, badSram, sizeof(badSram)) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + if (wc_PufEnroll(&ctx) != WC_NO_ERR_TRACE(PUF_ENROLL_E)) + return WC_TEST_RET_ENC_NC; + if (wc_PufReconstruct(&ctx, helperBuf, WC_PUF_HELPER_BYTES) + != WC_NO_ERR_TRACE(PUF_RECONSTRUCT_E)) + return WC_TEST_RET_ENC_NC; + /* only the readout is invalidated: output already derived from the + * readout that did pass stays available */ + ret = wc_PufGetIdentity(&ctx, id2, sizeof(id2)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (XMEMCMP(id2, id3, WC_PUF_ID_SZ) != 0) + return WC_TEST_RET_ENC_NC; + + /* the short-size path fails before any copy, so it has to invalidate too + * rather than leave the earlier readout enrollable */ + puf_fill_weight(badSram, (onesLow + onesHigh) / 2U); + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + ret = wc_PufReadSram(&ctx, badSram, sizeof(badSram)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + if (wc_PufReadSram(&ctx, badSram, WC_PUF_RAW_BYTES - 1) + != WC_NO_ERR_TRACE(PUF_READ_E)) + return WC_TEST_RET_ENC_NC; + if (wc_PufEnroll(&ctx) != WC_NO_ERR_TRACE(PUF_ENROLL_E)) + return WC_TEST_RET_ENC_NC; + + /* wc_PufSetTestData must keep bypassing the health test: the KAT vectors + * it injects have to reach the pipeline unfiltered, and a readout this + * degenerate proves the short-circuit in wc_PufReadSram is what lets them + * through. Nothing here derives a key from it. */ + XMEMSET(badSram, 0x00, sizeof(badSram)); + ret = wc_PufInit(&ctx); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + ret = wc_PufSetTestData(&ctx, badSram, sizeof(badSram)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + ret = wc_PufReadSram(&ctx, badSram, sizeof(badSram)); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); + + /* and still travels the full read -> enroll path on a context that never + * saw wc_PufSetTestData, reproducing the Test 2 identity */ + if (inBand) { + ret = wc_PufInit(&ctx); + 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); + 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; + } + return 0; #else return 0; diff --git a/wolfssl/wolfcrypt/puf.h b/wolfssl/wolfcrypt/puf.h index abb2fb826f..328f66afbc 100644 --- a/wolfssl/wolfcrypt/puf.h +++ b/wolfssl/wolfcrypt/puf.h @@ -42,6 +42,14 @@ 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. + Every readout passed to wc_PufReadSram() is health tested first (see + wc_PufCheckSram, and WC_PUF_HW_MIN_PCT / WC_PUF_HW_MAX_PCT for the + Hamming-weight band it must fall inside). A degenerate readout - all zero, + all ones, or a repeating block - is self-consistent through the whole + pipeline and would yield a key identical on every device, so it is + rejected with PUF_READ_E. The SRAM region must therefore be sampled from + reset, before .bss/.data init and before it is used as stack or heap. + Build: ./configure --enable-puf[=small|balanced|strong|strongest] (auto-enables HKDF). CMake: -DWOLFSSL_PUF=yes with -DWOLFSSL_PUF_PROFILE=small|balanced|strong|strongest and @@ -95,6 +103,36 @@ #define WC_PUF_BCH_T 10 #endif +/* Startup health-test band for the raw readout, as a percentage of + * WC_PUF_RAW_BITS. A readout whose Hamming weight falls outside this band is + * not plausible SRAM power-on noise (a cleared or written region, a stuck + * peripheral window), so wc_PufReadSram rejects it rather than deriving a + * device-independent key from it. Widen only if the target's SRAM is known to + * be strongly biased and that bias has been measured with wc_PufCheckSram. + * Note the band is a percentage while the spread of a healthy readout narrows + * as sqrt(WC_PUF_RAW_BITS): the default is ~13 sigma wide at 16 codewords but + * only ~3.4 sigma at 1, so a build with one or two codewords should widen it + * rather than accept the occasional false reject at boot. + * The band does not affect the helper-data format or the derived key, so it + * is deliberately not part of WC_PUF_PROFILE_ID. Set it through the build so + * an application sees the same band as the library: autotools records a + * CPPFLAGS -D in wolfssl/options.h, and CMake has + * -DWOLFSSL_PUF_HW_MIN_PCT / -DWOLFSSL_PUF_HW_MAX_PCT. A library-only + * user_settings.h does not carry across, so in that case take the verdict + * from wc_PufCheckSram() rather than recomputing the band. */ +#ifndef WC_PUF_HW_MIN_PCT + #define WC_PUF_HW_MIN_PCT 35 +#endif +#ifndef WC_PUF_HW_MAX_PCT + #define WC_PUF_HW_MAX_PCT 65 +#endif +#if WC_PUF_HW_MIN_PCT >= WC_PUF_HW_MAX_PCT + #error "WC_PUF_HW_MIN_PCT must be less than WC_PUF_HW_MAX_PCT" +#endif +#if WC_PUF_HW_MIN_PCT < 1 || WC_PUF_HW_MAX_PCT > 99 + #error "WC_PUF_HW_MIN_PCT/WC_PUF_HW_MAX_PCT must be within 1..99" +#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 */ @@ -135,9 +173,28 @@ #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_STRIDE_BITS 128 +#define WC_PUF_RAW_STRIDE_BYTES (WC_PUF_RAW_STRIDE_BITS / 8) +#define WC_PUF_RAW_BITS (WC_PUF_NUM_CODEWORDS * WC_PUF_RAW_STRIDE_BITS) #define WC_PUF_RAW_BYTES (WC_PUF_RAW_BITS / 8) +/* One codeword per stride, so the readout health test can walk the buffer in + * WC_PUF_CW_BYTES blocks. Every shipped profile pins n = 127, which is what + * makes the two equal. Divergence in either direction is a build failure: a + * larger codeword would read past the caller's region, a smaller one would + * leave the tail of every block untested. */ +#if WC_PUF_CW_BYTES != WC_PUF_RAW_STRIDE_BYTES + #error "WC_PUF_CW_BYTES must equal the raw readout stride" +#endif + +/* The band test compares ones * 100 against WC_PUF_RAW_BITS * pct in word32 + * arithmetic. The codeword cap above keeps both sides far inside that range + * (4095 codewords -> 524160 bits -> 52416000), but pin the bound so raising + * the cap fails the build here rather than silently wrapping the check. */ +#if WC_PUF_RAW_BITS > (0xFFFFFFFFU / 100U) + #error "WC_PUF_RAW_BITS too large for the health-test band arithmetic" +#endif + /* 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) @@ -217,6 +274,8 @@ typedef struct 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_PufCheckSram(const byte* sramAddr, word32 sramSz, + word32* onesCount); WOLFSSL_API int wc_PufEnroll(wc_PufCtx* ctx); WOLFSSL_API int wc_PufReconstruct(wc_PufCtx* ctx, const byte* helperData, word32 helperSz);