diff --git a/embedded/ti-c2000-f28p55x/Header/user_settings.h b/embedded/ti-c2000-f28p55x/Header/user_settings.h index 13389c6b..b3ba5741 100644 --- a/embedded/ti-c2000-f28p55x/Header/user_settings.h +++ b/embedded/ti-c2000-f28p55x/Header/user_settings.h @@ -570,15 +570,27 @@ extern long my_time(long* t); /* ------------------------------------------------------------------------- */ /* RNG - real SHA-256 Hash-DRBG seeded by a DEV-ONLY test seed */ /* ------------------------------------------------------------------------- */ -/* The F28P550SJ has no hardware TRNG, so there is no real entropy source. - * WOLFSSL_GENSEED_FORTEST makes random.c supply a built-in wc_GenerateSeed - * (an incrementing test value) that feeds the standard SHA-256 Hash-DRBG. - * This exercises the real DRBG code path (what a production build with a TRNG - * would use) and lets random_test pass - but the seed is NOT random, so this - * is DEV/TEST ONLY and MUST NOT be shipped. Replace wc_GenerateSeed with a - * real TRNG before any production use. */ +#ifdef WOLF_ENTROPY +/* Real entropy: the on-chip oscillator-jitter source. The F28P550SJ has no + * TRNG, but it does have two independent RC oscillators and a crystal-derived + * PLL, and a Dual-Clock Comparator that can count one against another. The + * LSB of that count is the noise bit; it is oversampled well past its measured + * min-entropy, health-tested per SP800-90B 4.4, SHA-256 conditioned, and fed + * to the same SHA-256 Hash-DRBG. See IDE/C2000/README.md in the wolfSSL tree + * for the on-hardware characterization. */ +#undef WOLFSSL_C2000_ENTROPY +#define WOLFSSL_C2000_ENTROPY +#else +/* The F28P550SJ has no hardware TRNG, so without ENTROPY=1 there is no real + * entropy source. WOLFSSL_GENSEED_FORTEST makes random.c supply a built-in + * wc_GenerateSeed (an incrementing test value) that feeds the standard SHA-256 + * Hash-DRBG. This exercises the real DRBG code path (what a production build + * with a TRNG would use) and lets random_test pass - but the seed is NOT + * random, so this is DEV/TEST ONLY and MUST NOT be shipped. Build with + * ENTROPY=1 for the real source. */ #undef WOLFSSL_GENSEED_FORTEST #define WOLFSSL_GENSEED_FORTEST +#endif /* Run every self-test to completion and report each, so macro_test (a 16-bit * safe-math self-test that currently fails on C28x) does not abort the suite diff --git a/embedded/ti-c2000-f28p55x/Makefile b/embedded/ti-c2000-f28p55x/Makefile index ca2a1afb..ac5289e6 100644 --- a/embedded/ti-c2000-f28p55x/Makefile +++ b/embedded/ti-c2000-f28p55x/Makefile @@ -277,6 +277,25 @@ ifeq ($(MEMPROF),1) CFLAGS += --define=WOLF_MEM_PROFILE endif +# ENTROPY=1 replaces the dev-only WOLFSSL_GENSEED_FORTEST counter with the real +# oscillator-jitter entropy source (DCC/INTOSC vs PLL, SHA-256 conditioned, +# SP800-90B health tests) feeding the SP800-90A Hash-DRBG. +ENTROPY ?= 0 +ifeq ($(ENTROPY),1) + CFLAGS += --define=WOLF_ENTROPY + WC_SRCS += $(WOLFROOT)/wolfcrypt/src/port/ti/ti-c2000-entropy.c +endif + +# ENTROPY_PROBE=1 builds the raw entropy-source characterization image: it +# dumps unconditioned DCC oscillator-jitter and ADC samples over SCI so a host +# can estimate min-entropy. Measurement only - no crypto runs. +ENTROPY_PROBE ?= 0 +ifeq ($(ENTROPY_PROBE),1) + CFLAGS += --define=WOLF_ENTROPY_PROBE --define=NO_CRYPT_TEST \ + --define=NO_CRYPT_BENCHMARK + HARNESS_EXTRA += $(CURDIR)/Source/entropy_probe.c +endif + # Append the shared AES core once, after every toggle has had its say. ifeq ($(NEED_AES_CORE),1) WC_SRCS += \ @@ -284,7 +303,7 @@ ifeq ($(NEED_AES_CORE),1) $(WOLFROOT)/wolfcrypt/src/wc_encrypt.c endif -ALL_SRCS := $(WC_SRCS) $(HARNESS_SRCS) $(BSP_SRCS) $(ASM_SRCS) +ALL_SRCS := $(WC_SRCS) $(HARNESS_SRCS) $(HARNESS_EXTRA) $(BSP_SRCS) $(ASM_SRCS) .PHONY: all clean diff --git a/embedded/ti-c2000-f28p55x/README.md b/embedded/ti-c2000-f28p55x/README.md index cd623781..bdcfe8e0 100644 --- a/embedded/ti-c2000-f28p55x/README.md +++ b/embedded/ti-c2000-f28p55x/README.md @@ -58,6 +58,8 @@ Each is `make =1` (default 0 unless noted), additive on top of the default | `X25519=1` | Curve25519 (X25519) + Ed25519 | | `HKDF=1` | HMAC + HKDF (RFC 2104 / RFC 5869) | | `CHACHA=1` | ChaCha20-Poly1305 AEAD (RFC 8439) | +| `ENTROPY=1` | Real oscillator-jitter entropy source (DCC/INTOSC vs PLL) in place of the dev-only test seed. See "RNG and entropy" below | +| `ENTROPY_PROBE=1` | Raw entropy characterization image: dumps unconditioned samples over SCI for host analysis, runs no crypto | | `RSA=1` | RSA-2048 verify (SP math, 2048-only, verify/public-only) | | `SIGN=1` | Full ML-DSA-87 keygen+sign+verify demo (dedicated linker script, 32 KW heap, no test/bench harness) | | `BENCH=1` | Run only `benchmark` instead of `wolfcrypt_test` (they need separate images on this RAM-limited part) | @@ -113,19 +115,43 @@ Measured at 150 MHz (`make HWAES=1 BENCH=1`, which prints paired `SW`/`HW` rows) | Operation | Software | AESA | Speedup | |---|---|---|---| -| AES-128-ECB encrypt | 471 KiB/s | 2.37 MiB/s | 5.2x | -| AES-256-ECB encrypt | 377 KiB/s | 2.32 MiB/s | 6.3x | -| AES-128-CBC encrypt | 405 KiB/s | 2.36 MiB/s | 6.0x | -| AES-128-CBC decrypt | 388 KiB/s | 2.34 MiB/s | 6.2x | -| AES-256-CBC encrypt | 333 KiB/s | 2.31 MiB/s | 7.1x | -| AES-256-CBC decrypt | 322 KiB/s | 2.29 MiB/s | 7.3x | +| AES-128-ECB encrypt | 471 KiB/s | 2.39 MiB/s | 5.2x | +| AES-256-ECB encrypt | 377 KiB/s | 2.34 MiB/s | 6.3x | +| AES-128-CBC encrypt | 405 KiB/s | 2.37 MiB/s | 6.0x | +| AES-128-CBC decrypt | 388 KiB/s | 2.36 MiB/s | 6.2x | +| AES-256-CBC encrypt | 333 KiB/s | 2.32 MiB/s | 7.1x | +| AES-256-CBC decrypt | 322 KiB/s | 2.31 MiB/s | 7.3x | | AES-128-CTR | 408 KiB/s | 1.45 MiB/s | 3.6x | -| AES-256-CTR | 335 KiB/s | 1.44 MiB/s | 4.4x | +| AES-256-CTR | 335 KiB/s | 1.45 MiB/s | 4.4x | AES-GCM barely moves (~32 to ~34 KiB/s): only its internal ECB calls reach the accelerator and the `GCM_SMALL` byte-wise GHASH dominates. Using the block's own GCM mode is future work. CFB, CCM, CMAC and everything else stay in software -- the callback returns `CRYPTOCB_UNAVAILABLE` and wolfCrypt falls through transparently. Two hardware quirks are documented in `IDE/C2000/README.md` in the wolfSSL tree and worth knowing before touching this code: driverlib expects little-endian octets within each 32-bit word (not a raw cast of a `byte*`), and the block's CTR counter increment does **not** match wolfCrypt's big-endian 128-bit `IncrementAesCounter()` once an increment carries across an octet boundary -- so the port drives the accelerator in ECB mode and keeps the counter in software. Both quirks produce a *correct first block*, which is why the multi-block cases in the harness matter. -## RNG caveat +## RNG and entropy -The F28P550SJ has **no hardware TRNG**. This build uses `WOLFSSL_GENSEED_FORTEST` (random.c's built-in incrementing test seed feeding the real SHA-256 Hash-DRBG): it exercises the real DRBG path but is **development-only, not cryptographically secure**. For production, wire a real entropy source into `wc_GenerateSeed()`. +The F28P550SJ has **no hardware TRNG**. It does have three independent oscillators -- INTOSC1 and INTOSC2 (on-chip ~10 MHz RC) and the external crystal behind SYSCLK/PLLRAWCLK -- and two Dual-Clock Comparators that can count one against another. `ENTROPY=1` uses that: a DCC counts PLLRAWCLK edges inside a window of INTOSC cycles, and the LSB of the count is one noise bit carrying the relative phase drift of two physically distinct oscillators. The raw stream is oversampled well past its measured min-entropy, health-tested per SP800-90B 4.4, SHA-256 conditioned, and fed to the SP800-90A Hash-DRBG. + +Measured on this board with `ENTROPY_PROBE=1` (262144 raw bits per source, LSB extraction, host analysis): + +| Source | Hmin/bit | bias | max \|acf\| | chi-square p | +|---|---|---|---|---| +| INTOSC1 window / PLL counted (DCC1) | **0.932** | -0.0000 | 0.005 | 0.623 | +| INTOSC2 window / PLL counted (DCC0) | 0.843 | -0.0027 | 0.005 | 0.000 | +| ADC LSB, floating input | 0.834 | -0.0086 | 0.073 | 0.000 | + +Only INTOSC1 is credited toward the entropy budget; INTOSC2 is hashed in as defence-in-depth but fails a chi-square uniformity check decisively, and the ADC source is off by default because it also fails chi-square and depends on a spare analog pin being left floating. The port assumes 0.5 bits per raw bit and oversamples 2x on top, about a 4x cushion. This is a most-common-value estimate with bias and correlation screening, **not** a full SP800-90B non-IID assessment. Read 0.932 against the estimator's ceiling rather than 1.0: at this sample count a synthetic uniform stream estimates to 0.930, so the credited source is statistically indistinguishable from uniform. These are single-run measurements of a physical source and move slightly between runs (an earlier capture gave 0.924 / 0.775 / 0.865), but the pass/fail conclusions have been identical in every run. + +Only the DCC measurement itself is C2000 code. The SP800-90B startup and continuous health tests, the entropy budget, the SHA-256 conditioner and the latched fail-closed state come from wolfSSL's generic `wc_NoiseSrc_*` layer in `wolfcrypt/src/random.c`, which `WOLFSSL_C2000_ENTROPY` configures. See `IDE/C2000/README.md` in the wolfSSL tree. + +`ENTROPY_PROBE=1` builds the measurement image itself: it dumps unconditioned samples over the SCI console for host analysis, and runs no crypto. `tools/entropy_analyze.py` (numpy only) consumes that capture and reproduces the table above: + +``` +make CGT_ROOT= ENTROPY_PROBE=1 +# flash, run, capture the console to probe.log, then: +python3 tools/entropy_analyze.py probe.log +``` + +Min-entropy is the SP800-90B 6.3.1 most-common-value estimate over the 8-bit octet alphabet at the 99% upper confidence bound, divided by 8 to express it per bit. The octet alphabet is used rather than the bit alphabet because it also catches structure across adjacent bits that a per-bit estimate cannot see. Run `python3 tools/entropy_analyze.py --selftest` to check the estimators against synthetic streams with known properties; that also calibrates the ceiling, since at this sample count a genuinely uniform stream estimates to about 0.93 rather than 1.0 -- so the credited source's 0.92 is at the estimator's practical maximum, not 8% short of ideal. + +Without `ENTROPY=1` the build falls back to `WOLFSSL_GENSEED_FORTEST` (random.c's incrementing test seed feeding the real Hash-DRBG) -- it exercises the DRBG path but is **development-only and not cryptographically secure**. diff --git a/embedded/ti-c2000-f28p55x/Source/entropy_probe.c b/embedded/ti-c2000-f28p55x/Source/entropy_probe.c new file mode 100644 index 00000000..2554f538 --- /dev/null +++ b/embedded/ti-c2000-f28p55x/Source/entropy_probe.c @@ -0,0 +1,252 @@ +/* entropy_probe.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 + */ + +/* Raw entropy-source characterization for the TMS320F28P550SJ. + * + * `make ENTROPY_PROBE=1`. A MEASUREMENT tool, not part of the RNG: it dumps + * unconditioned samples over SCI so a host can estimate min-entropy before + * anything is wired into wc_GenerateSeed(). + * + * Candidates: DCC oscillator jitter (a DCC counts PLL edges inside a window of + * INTOSC cycles, leaving the drift between two independent oscillators in the + * low bits) and ADC LSB noise (floating high-Z input, short acquisition so the + * SAR never settles). + * + * Tagged hex output for the host analyzer: + * E0/E1 DCC1 INTOSC1 / DCC0 INTOSC2 window, PLL counted, N cycles + * E3 0 ADCC raw 12-bit results + * E4/E5/E6 packed LSB streams (8 samples per emitted octet) + * PROBE DONE + */ + +#include +#include + +#include "driverlib.h" +#include "device.h" + +#ifdef WOLF_ENTROPY_PROBE + +/* Counter1 counts down from 0xFFFFF, capping the window near 34,900 INTOSC + * cycles at 300 MHz PLL / 10 MHz INTOSC; keep well under that. */ +#define PROBE_CNT1_SEED 0xFFFFFUL +#define PROBE_SAMPLES 1024 +/* printf over SCI dominates; keep the ADC set smaller. */ +#define PROBE_ADC_SAMPLES 1024 +#define PROBE_PER_LINE 16 +/* Packed-LSB stream size. 32 KiB = 262144 bits per source, enough for the + * MCV confidence bound to stop being the limiting factor. */ +#define PROBE_PACKED_BYTES 32768UL + +/* Window sweep (slow-clock cycles). Entropy per sample grows with the window + * while the rate falls, so the useful entropy rate peaks in the middle. */ +static const uint32_t probeWindows[] = { 256UL, 1024UL, 4096UL }; +#define PROBE_NUM_WINDOWS (sizeof(probeWindows) / sizeof(probeWindows[0])) + + +/* One DCC measurement, at register level: DCC_measureClockFrequency() uses + * float32_t, which does not belong here. */ +static uint32_t probe_dccSample(uint32_t base, DCC_Count0ClockSource src0, + DCC_Count1ClockSource src1, uint32_t window) +{ + uint32_t guard; + + DCC_clearErrorFlag(base); + DCC_clearDoneFlag(base); + DCC_disableModule(base); + DCC_disableErrorSignal(base); + DCC_disableDoneSignal(base); + + DCC_setCounter0ClkSource(base, src0); + DCC_setCounter1ClkSource(base, src1); + DCC_setCounterSeeds(base, window, DCC_VALIDSEED_MIN, PROBE_CNT1_SEED); + DCC_enableSingleShotMode(base, DCC_MODE_COUNTER_ZERO); + + /* DONE only latches with the done/error signals enabled - driverlib's own + * DCC_measureClockFrequency() does this and it is easy to miss. */ + DCC_enableErrorSignal(base); + DCC_enableDoneSignal(base); + + DCC_enableModule(base); + + /* Bounded wait, scaled to the window, so a bad mux cannot hang. */ + for (guard = 0; guard < (window * 256UL) + 100000UL; guard++) { + if (DCC_getSingleShotStatus(base) || DCC_getErrorStatus(base)) { + break; + } + } + + return (PROBE_CNT1_SEED - (DCC_getCounter1Value(base) & PROBE_CNT1_SEED)); +} + + +static void probe_dccInit(void) +{ + SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_DCC0); + SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_DCC1); + SysCtl_delay(100); +} + + +static void probe_adcInit(void) +{ + SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_ADCC); + SysCtl_delay(100); + + ASysCtl_setAnalogReferenceInternal(ASYSCTL_ANAREF_INTREF_ADCC); + + ADC_setPrescaler(ADCC_BASE, ADC_CLK_DIV_4_0); + ADC_setInterruptPulseMode(ADCC_BASE, ADC_PULSE_END_OF_CONV); + ADC_enableConverter(ADCC_BASE); + DEVICE_DELAY_US(1000); + + /* Short acquisition on a floating high-Z input: the SAR deliberately does + * not settle, which is where the noise comes from. */ + ADC_setupSOC(ADCC_BASE, ADC_SOC_NUMBER0, ADC_TRIGGER_SW_ONLY, + ADC_CH_ADCIN0, 8U); + ADC_setInterruptSource(ADCC_BASE, ADC_INT_NUMBER1, ADC_SOC_NUMBER0); + ADC_enableInterrupt(ADCC_BASE, ADC_INT_NUMBER1); + ADC_clearInterruptStatus(ADCC_BASE, ADC_INT_NUMBER1); +} + + +static uint16_t probe_adcSample(void) +{ + uint32_t guard; + + ADC_clearInterruptStatus(ADCC_BASE, ADC_INT_NUMBER1); + ADC_forceSOC(ADCC_BASE, ADC_SOC_NUMBER0); + + for (guard = 0; guard < 1000000UL; guard++) { + if (ADC_getInterruptStatus(ADCC_BASE, ADC_INT_NUMBER1)) { + break; + } + } + + return ADC_readResult(ADCCRESULT_BASE, ADC_SOC_NUMBER0); +} + + +static void probe_dumpDcc(const char* tag, uint32_t base, + DCC_Count0ClockSource src0, + DCC_Count1ClockSource src1, uint32_t window) +{ + uint32_t i; + + for (i = 0; i < PROBE_SAMPLES; i++) { + if ((i % PROBE_PER_LINE) == 0) { + printf("\r\n%s %lu ", tag, (unsigned long)window); + } + printf("%05lx ", + (unsigned long)probe_dccSample(base, src0, src1, window)); + } + printf("\r\n"); +} + + +/* Packed LSB stream. A useful min-entropy estimate needs far more samples + * than a 115200 UART can carry one hex count at a time, so the bit extraction + * happens on-target: 8 samples per emitted octet. This is also the stream a + * real entropy source consumes, so it is the right thing to assess. */ +static void probe_dumpPackedDcc(const char* tag, uint32_t base, + DCC_Count0ClockSource src0, + DCC_Count1ClockSource src1, uint32_t window, + uint32_t nbytes) +{ + uint32_t i; + int b; + uint16_t acc; + + for (i = 0; i < nbytes; i++) { + if ((i % 32U) == 0U) { + printf("\r\n%s %lu ", tag, (unsigned long)window); + } + acc = 0U; + for (b = 0; b < 8; b++) { + acc = (uint16_t)(acc | + (uint16_t)((probe_dccSample(base, src0, src1, window) & 1U) + << b)); + } + printf("%02x ", (unsigned int)(acc & 0xFFU)); + } + printf("\r\n"); +} + + +static void probe_dumpPackedAdc(uint32_t nbytes) +{ + uint32_t i; + int b; + uint16_t acc; + + for (i = 0; i < nbytes; i++) { + if ((i % 32U) == 0U) { + printf("\r\nE6 0 "); + } + acc = 0U; + for (b = 0; b < 8; b++) { + acc = (uint16_t)(acc | + (uint16_t)((probe_adcSample() & 1U) << b)); + } + printf("%02x ", (unsigned int)(acc & 0xFFU)); + } + printf("\r\n"); +} + + +void entropy_probe_run(void) +{ + uint32_t w; + uint32_t i; + + printf("\r\n=== ENTROPY PROBE ===\r\n"); + printf("SYSCLK %lu Hz, samples/config %d\r\n", + (unsigned long)DEVICE_SYSCLK_FREQ, (int)PROBE_SAMPLES); + + probe_dccInit(); + probe_adcInit(); + + for (w = 0; w < PROBE_NUM_WINDOWS; w++) { + probe_dumpDcc("E0", DCC1_BASE, DCC_COUNT0SRC_INTOSC1, + DCC_COUNT1SRC_PLL, probeWindows[w]); + probe_dumpDcc("E1", DCC0_BASE, DCC_COUNT0SRC_INTOSC2, + DCC_COUNT1SRC_PLL, probeWindows[w]); + } + + for (i = 0; i < PROBE_ADC_SAMPLES; i++) { + if ((i % PROBE_PER_LINE) == 0) { + printf("\r\nE3 0 "); + } + printf("%05lx ", (unsigned long)probe_adcSample()); + } + printf("\r\n"); + + /* Packed LSB streams for the real min-entropy assessment. */ + probe_dumpPackedDcc("E4", DCC1_BASE, DCC_COUNT0SRC_INTOSC1, + DCC_COUNT1SRC_PLL, 256UL, PROBE_PACKED_BYTES); + probe_dumpPackedDcc("E5", DCC0_BASE, DCC_COUNT0SRC_INTOSC2, + DCC_COUNT1SRC_PLL, 256UL, PROBE_PACKED_BYTES); + probe_dumpPackedAdc(PROBE_PACKED_BYTES); + + printf("\r\nPROBE DONE\r\n"); +} + +#endif /* WOLF_ENTROPY_PROBE */ diff --git a/embedded/ti-c2000-f28p55x/Source/wolf_main.c b/embedded/ti-c2000-f28p55x/Source/wolf_main.c index e1a893f6..51e383c2 100644 --- a/embedded/ti-c2000-f28p55x/Source/wolf_main.c +++ b/embedded/ti-c2000-f28p55x/Source/wolf_main.c @@ -58,6 +58,10 @@ #ifdef WOLF_HWAES #include #endif +#ifdef WOLF_ENTROPY +#include +#include +#endif #ifdef WOLF_25519 #include #include @@ -1093,6 +1097,69 @@ static void wolf_mlkem768_test(void) } #endif /* WOLF_MLKEM */ +#ifdef WOLF_ENTROPY +/* On-target validation of the entropy source. Beyond the port's SP800-90B + * health tests, this screens what a stuck or test-only source would fail: the + * raw noise is neither constant nor grossly biased, and the DRBG seeds. The + * bit-count check is coarse - the real min-entropy assessment is the host + * analysis of an ENTROPY_PROBE=1 capture (wolfSSL IDE/C2000/README.md). */ +static void wolf_entropy_test(void) +{ + static byte raw[256]; + static byte s1[32], s2[32]; + WC_RNG rng; + word32 ones; + word32 i; + int b; + int ret; + int src; + + ret = wc_c2000_Entropy_Init(); + printf("Entropy init + startup health test: %s (ret=%d)\r\n", + (ret == 0) ? "PASS" : "FAIL", ret); + if (ret != 0) { + return; + } + + ret = wc_c2000_Entropy_SelfTest(); + printf("Entropy liveness self-test (raw): %s (ret=%d)\r\n", + (ret == 0) ? "PASS" : "FAIL", ret); + + /* Raw noise sanity per source: population count should sit near half. */ + for (src = 0; src < 2; src++) { + ret = wc_c2000_Entropy_GetRaw(raw, (word32)sizeof(raw), src); + ones = 0; + for (i = 0; i < (word32)sizeof(raw); i++) { + for (b = 0; b < 8; b++) { + if ((raw[i] >> b) & 1) { + ones++; + } + } + } + /* 2048 bits; accept 40%..60% ones, i.e. counts 820..1228. */ + printf("Entropy raw src%d bit balance: %s (%lu/2048 ones)\r\n", + src, + (ret == 0 && ones > 819UL && ones < 1229UL) ? "PASS" : "FAIL", + (unsigned long)ones); + } + + /* End to end: the DRBG must seed and produce differing blocks. */ + ret = wc_InitRng(&rng); + printf("wc_InitRng with real entropy: %s (ret=%d)\r\n", + (ret == 0) ? "PASS" : "FAIL", ret); + if (ret == 0) { + ret = wc_RNG_GenerateBlock(&rng, s1, (word32)sizeof(s1)); + if (ret == 0) { + ret = wc_RNG_GenerateBlock(&rng, s2, (word32)sizeof(s2)); + } + printf("RNG blocks differ: %s\r\n", + (ret == 0 && XMEMCMP(s1, s2, sizeof(s1)) != 0) + ? "PASS" : "FAIL"); + wc_FreeRng(&rng); + } +} +#endif /* WOLF_ENTROPY */ + #ifdef WOLF_AES static void wolf_aes_test(void) { @@ -1899,6 +1966,15 @@ int main(void) printf("\r\n"); printf("=== wolfSSL wolfCrypt on TI C2000 LAUNCHXL-F28P55X ===\r\n"); +#ifdef WOLF_ENTROPY_PROBE + /* Measurement-only image: dump raw entropy samples and stop. */ + { + extern void entropy_probe_run(void); + entropy_probe_run(); + } + while (1) { + } +#endif #ifdef WOLF_MEM_PROFILE /* Route XMALLOC/XFREE/XREALLOC through the heap high-water tracker. */ @@ -1959,6 +2035,11 @@ int main(void) #endif #endif /* WOLF_MLKEM */ +#ifdef WOLF_ENTROPY + printf("\r\n--- Entropy (oscillator jitter) ---\r\n"); + wolf_entropy_test(); +#endif /* WOLF_ENTROPY */ + #ifdef WOLF_AES printf("\r\n--- AES (CBC/CTR/CFB/GCM) ---\r\n"); wolf_aes_test(); diff --git a/embedded/ti-c2000-f28p55x/tools/entropy_analyze.py b/embedded/ti-c2000-f28p55x/tools/entropy_analyze.py new file mode 100755 index 00000000..65493aa3 --- /dev/null +++ b/embedded/ti-c2000-f28p55x/tools/entropy_analyze.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Analyze the ENTROPY_PROBE=1 capture from the TI C2000 (C28x) entropy probe. + +Consumes the tagged lines that Source/entropy_probe.c emits over the SCI +console and reports, per noise source, the four figures published in the port +README: min-entropy per bit, per-bit bias, peak autocorrelation over lags +1..64, and a chi-square uniformity p-value. + +Tags produced by the probe: + E0 ... raw DCC1 count, INTOSC1 window / PLL counted + E1 ... raw DCC0 count, INTOSC2 window / PLL counted + E3 0 ... raw ADC result, floating input + E4 256 ... packed LSB stream, INTOSC1 (the credited source) + E5 256 ... packed LSB stream, INTOSC2 + E6 0 ... packed LSB stream, ADC + +The E4/E5/E6 streams are what the analysis uses: the bit extraction happens +on-target (8 samples per emitted octet) because a useful min-entropy estimate +needs far more samples than the UART can carry one hex count at a time, and +because that packed stream is exactly what the entropy source consumes. + +Method. Min-entropy is the SP800-90B 6.3.1 most-common-value estimate taken +over the 8-bit octet alphabet at the 99% upper confidence bound, divided by 8 +to express it per bit; the octet alphabet is used rather than the bit alphabet +because it also catches structure across adjacent bits, which a per-bit +estimate cannot see. This is an MCV estimate plus bias and correlation +screening, NOT a full SP800-90B non-IID assessment: MCV assumes IID, so it is +an upper bound, and low measured correlation is what makes it a reasonable one. + +Usage: + python3 tools/entropy_analyze.py capture.log + python3 tools/entropy_analyze.py --selftest + tail -f /tmp/uart-monitor/latest/ttyACMx.log | python3 tools/entropy_analyze.py - +""" + +import math +import re +import sys +from collections import OrderedDict + +try: + import numpy as np +except ImportError: + sys.exit("numpy is required: pip install numpy") + +# Packed-LSB streams, in report order, with the labels the README table uses. +PACKED = OrderedDict(( + ("E4", "INTOSC1 window / PLL counted (DCC1)"), + ("E5", "INTOSC2 window / PLL counted (DCC0)"), + ("E6", "ADC LSB, floating input"), +)) +RAW = OrderedDict(( + ("E0", "INTOSC1 raw DCC count"), + ("E1", "INTOSC2 raw DCC count"), + ("E3", "ADC raw result"), +)) + +BANNER = "=== ENTROPY PROBE ===" +DONE = "PROBE DONE" +MAX_LAG = 64 +Z_99 = 2.5758293035489004 # two-sided 99% normal quantile + + +def first_pass(text): + """The board loops main(), so the probe output repeats. Return just the + first complete pass, so a long capture does not silently concatenate + several runs into one sample set.""" + start = text.find(BANNER) + if start < 0: + return text + end = text.find(DONE, start) + if end < 0: + sys.stderr.write("warning: no '%s' marker - capture may be truncated\n" + % DONE) + return text[start:] + return text[start:end] + + +def parse(text): + """tag -> list of ints, in emission order.""" + out = {} + # A console line is " ...". Match per line, and + # never across a newline: the tags and window counts are themselves valid + # hex, so a multi-line match would swallow the next line's header as data. + # Tolerate any timestamp or prefix a log wrapper put ahead of the tag. + line_re = re.compile(r"\b(E[0-9])[^\S\n]+(\d+)[^\S\n]+" + r"((?:[0-9a-fA-F]+[^\S\n]*)+)$") + for line in text.splitlines(): + m = line_re.search(line.rstrip()) + if m is None: + continue + out.setdefault(m.group(1), []).extend( + int(t, 16) for t in m.group(3).split()) + return out + + +def unpack_bits(octets): + """Octets back to the LSB-first bit stream the probe packed.""" + a = np.asarray(octets, dtype=np.uint8) + return np.unpackbits(a[:, None], axis=1, bitorder="little").ravel() + + +def mcv_min_entropy(symbols, alphabet): + """SP800-90B 6.3.1 most-common-value estimate, 99% upper bound, in bits + per symbol.""" + n = len(symbols) + if n < 2: + return float("nan") + counts = np.bincount(np.asarray(symbols, dtype=np.int64), + minlength=alphabet) + p_hat = counts.max() / n + p_u = min(1.0, p_hat + Z_99 * math.sqrt(p_hat * (1.0 - p_hat) / (n - 1))) + return -math.log2(p_u) + + +def max_abs_acf(bits, max_lag=MAX_LAG): + """Peak |autocorrelation| over lags 1..max_lag of the bit stream.""" + x = np.asarray(bits, dtype=np.float64) + x = x - x.mean() + denom = float(np.dot(x, x)) + if denom == 0.0: + # Constant stream: no correlation is defined. Return the same + # (value, lag) shape callers unpack - a stuck source is exactly the + # case that must report cleanly rather than raise. + return float("nan"), 0 + peak, at = 0.0, 0 + for lag in range(1, min(max_lag, len(x) - 1) + 1): + r = abs(float(np.dot(x[:-lag], x[lag:])) / denom) + if r > peak: + peak, at = r, lag + return peak, at + + +def _gamma_q(s, x): + """Regularized upper incomplete gamma Q(s,x), by the series for P(s,x) + when x < s+1 and Lentz's continued fraction for Q(s,x) otherwise. Written + out because scipy is not assumed present and the Wilson-Hilferty + approximation, while fine in the tails, is off by ~0.01 near the median - + and a uniformity p-value in the middle of the range is exactly what gets + published.""" + if x < 0.0 or s <= 0.0: + return float("nan") + if x == 0.0: + return 1.0 + + if x < s + 1.0: # series for P(s,x), Q = 1 - P + term = 1.0 / s + total = term + n = s + for _ in range(1000): + n += 1.0 + term *= x / n + total += term + if abs(term) < abs(total) * 1e-16: + break + return 1.0 - total * math.exp(-x + s * math.log(x) - math.lgamma(s)) + + tiny = 1e-300 # continued fraction for Q(s,x) + b = x + 1.0 - s + c = 1.0 / tiny + d = 1.0 / b + h = d + for i in range(1, 1000): + an = -i * (i - s) + b += 2.0 + d = an * d + b + if abs(d) < tiny: + d = tiny + c = b + an / c + if abs(c) < tiny: + c = tiny + d = 1.0 / d + delta = d * c + h *= delta + if abs(delta - 1.0) < 1e-16: + break + return h * math.exp(-x + s * math.log(x) - math.lgamma(s)) + + +def chi2_sf(x, k): + """Upper tail of chi-square(k).""" + if k <= 0: + return float("nan") + return _gamma_q(k / 2.0, x / 2.0) + + +def chi2_uniform_octets(octets): + """Chi-square goodness of fit of the octet histogram against uniform.""" + n = len(octets) + counts = np.bincount(np.asarray(octets, dtype=np.int64), minlength=256) + expected = n / 256.0 + stat = float(((counts - expected) ** 2 / expected).sum()) + return stat, chi2_sf(stat, 255) + + +def report(tag, label, octets): + bits = unpack_bits(octets) + n_bits = len(bits) + ones = int(bits.sum()) + bias = ones / n_bits - 0.5 + + h_octet = mcv_min_entropy(octets, 256) + h_per_bit = h_octet / 8.0 + h_bitwise = mcv_min_entropy(bits, 2) + acf, acf_lag = max_abs_acf(bits) + chi_stat, chi_p = chi2_uniform_octets(octets) + + print("%s %s" % (tag, label)) + print(" samples %d octets (%d bits)" % (len(octets), n_bits)) + print(" Hmin/bit %.3f (octet MCV %.3f bits / 8)" + % (h_per_bit, h_octet)) + print(" Hmin/bit bitwise %.3f (bit-alphabet MCV, less conservative)" + % h_bitwise) + print(" bias %.4f (%d/%d ones)" % (bias, ones, n_bits)) + print(" max |acf| 1..%-3d %.3f (at lag %d)" % (MAX_LAG, acf, acf_lag)) + print(" chi-square p %.3f (stat %.1f, df 255)" + % (chi_p, chi_stat)) + print() + return dict(tag=tag, label=label, h=h_per_bit, bias=bias, acf=acf, + p=chi_p) + + +def selftest(): + """Synthetic streams with known properties, so the estimators can be + trusted before they are pointed at real silicon. Also calibrates the + ceiling: at this sample count the octet-MCV/8 estimate of a genuinely + uniform stream lands near 0.93, not 1.0, so a measured 0.92 is at the + estimator's practical maximum rather than 8% short of ideal.""" + # chi2_sf is hand-rolled (no scipy), so check it against known quantiles + # before anything relies on the p-values it produces. + known = [(1.0, 1, 0.317311), (10.0, 10, 0.440493), + (3.841459, 1, 0.05), (18.307038, 10, 0.05), + (293.2478, 255, 0.05), (310.4574, 255, 0.01), + (284.3359, 255, 0.10)] + worst = max(abs(chi2_sf(x, k) - want) for x, k, want in known) + print("chi2_sf worst error vs known quantiles: %.2e %s\n" + % (worst, "OK" if worst < 2e-4 else "FAIL")) + + rng = np.random.default_rng(1234) + n = 32768 + print("Estimator self-test (%d octets per case)\n" % n) + + uniform = rng.integers(0, 256, n) + report("--", "uniform (ceiling: Hmin ~0.93, p uniform, acf ~0)", uniform) + + bits = (rng.random(n * 8) < 0.55).astype(np.uint8) + report("--", "biased p(1)=0.55 (expect lower Hmin, bias ~0.05, p=0)", + np.packbits(bits.reshape(-1, 8), axis=1, bitorder="little").ravel()) + + x = np.zeros(n * 8, dtype=np.uint8) + for i in range(1, len(x)): + x[i] = x[i - 1] if rng.random() < 0.85 else 1 - x[i - 1] + report("--", "lag-1 correlated, unbiased marginal " + "(bitwise MCV is blind to this; octet MCV and acf are not)", + np.packbits(x.reshape(-1, 8), axis=1, bitorder="little").ravel()) + + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else "-" + if path == "--selftest": + selftest() + return + if path == "-": + text = sys.stdin.read() + else: + with open(path, errors="replace") as f: + text = f.read() + + data = parse(first_pass(text)) + if not data: + sys.exit("no probe tags found - is this an ENTROPY_PROBE=1 capture?") + + rows = [] + for tag, label in PACKED.items(): + octets = data.get(tag) + if not octets: + sys.stderr.write("warning: no %s samples (%s)\n" % (tag, label)) + continue + bad = [v for v in octets if v > 0xFF] + if bad: + sys.exit("%s: %d values exceed one octet - capture is corrupt" + % (tag, len(bad))) + rows.append(report(tag, label, octets)) + + for tag, label in RAW.items(): + vals = data.get(tag) + if vals: + a = np.asarray(vals, dtype=np.int64) + print("%s %s: %d samples, min %d max %d mean %.1f, " + "%d distinct" % (tag, label, len(a), a.min(), a.max(), + a.mean(), len(np.unique(a)))) + print() + + print("README table:") + print() + print("| Source | Hmin/bit | bias | max \\|acf\\| lag 1..%d | chi-square p |" + % MAX_LAG) + print("|---|---|---|---|---|") + for r in rows: + print("| %s | %.3f | %.4f | %.3f | %.3f |" + % (r["label"], r["h"], r["bias"], r["acf"], r["p"])) + + +if __name__ == "__main__": + main()