mirror of https://github.com/wolfSSL/wolfssl.git
wolfCrypt SRAM PUF: health test the raw readout before accepting it
parent
89541d1110
commit
c5c63c0ba5
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
22
ChangeLog.md
22
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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue