From 8fca85fa5720cbe6d203e5f7b7e99f49a5caa35f Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 13 Aug 2026 09:45:52 -0700 Subject: [PATCH 1/2] TI C2000 example: HWAES=1 AESA offload with HW-vs-SW cross-KATs --- .../ti-c2000-f28p55x/Header/user_settings.h | 30 +++ embedded/ti-c2000-f28p55x/Makefile | 49 +++-- embedded/ti-c2000-f28p55x/README.md | 30 ++- embedded/ti-c2000-f28p55x/Source/wolf_main.c | 173 ++++++++++++++++++ 4 files changed, 267 insertions(+), 15 deletions(-) diff --git a/embedded/ti-c2000-f28p55x/Header/user_settings.h b/embedded/ti-c2000-f28p55x/Header/user_settings.h index 9e692af77..13389c6b5 100644 --- a/embedded/ti-c2000-f28p55x/Header/user_settings.h +++ b/embedded/ti-c2000-f28p55x/Header/user_settings.h @@ -329,6 +329,36 @@ extern "C" { #define NO_AES #endif +#ifdef WOLF_HWAES +/* Offload AES-ECB/CBC/CTR to the on-chip AESA block through the crypto + * callback framework. Software AES stays compiled in: a context opts into + * hardware with wc_AesInit(&aes, NULL, WOLFSSL_C2000_DEVID), while one + * initialised with INVALID_DEVID stays pure software. That is what lets the + * KAT harness cross-check the two in a single image, so deliberately do NOT + * define WOLF_CRYPTO_CB_ONLY_AES. */ +#undef WOLF_CRYPTO_CB +#define WOLF_CRYPTO_CB +#undef WOLFSSL_C2000_AES +#define WOLFSSL_C2000_AES + +/* HAVE_AES_ECB is what compiles wc_AesEcbEncrypt/Decrypt and, with it, the ECB + * crypto-callback hook the hardware port needs; WOLFSSL_AES_DIRECT alone only + * creates the callback plumbing, not the entry points. Kept inside the HWAES + * block: it also switches the software CTR path to the bulk-ECB strategy and + * costs code size, so a software-only AES=1 build should not pay for it. */ +#undef HAVE_AES_ECB +#define HAVE_AES_ECB + +/* Single source of truth for the AESA device id. ti-c2000.h defaults this to + * 0x2000 behind #ifndef, so setting it here wins and lets WC_USE_DEVID be + * derived from it: wolfcrypt_test and benchmark then target the same device + * the KAT harness passes to wc_AesInit(), with no literal to keep in sync. */ +#undef WOLFSSL_C2000_DEVID +#define WOLFSSL_C2000_DEVID 0x2000 +#undef WC_USE_DEVID +#define WC_USE_DEVID WOLFSSL_C2000_DEVID +#endif + /* Curve25519 (X25519) + Ed25519. Enabled with EXTRA_CFLAGS=--define=WOLF_25519 * (X25519=1 build). No __uint128_t and no SP-25519 backend on C28x, so the * default fe[10] 32-bit-limb field arithmetic is used; Ed25519 reuses the diff --git a/embedded/ti-c2000-f28p55x/Makefile b/embedded/ti-c2000-f28p55x/Makefile index ce4f86b9c..ca2a1afbf 100644 --- a/embedded/ti-c2000-f28p55x/Makefile +++ b/embedded/ti-c2000-f28p55x/Makefile @@ -36,13 +36,16 @@ endif CL := $(CGT_ROOT)/bin/cl2000 +# Header/ must precede $(WOLFROOT): wolfSSL's documented user_settings.h +# workflow puts one at the wolfSSL tree root, which would otherwise shadow this +# example's and silently build a different configuration. INCS := \ -I$(CGT_ROOT)/include \ -I$(DRV) \ -I$(DEV)/common/include \ -I$(DEV)/headers/include \ - -I$(WOLFROOT) \ - -I$(CURDIR)/Header + -I$(CURDIR)/Header \ + -I$(WOLFROOT) # --float_support=fpu32 and --abi=eabi must match the prebuilt driverlib.lib. # Define WOLF_C2000_SCI_STDOUT to route printf to SCIA (XDS110 COM); omit it to @@ -108,13 +111,16 @@ ifeq ($(MLKEM),1) $(WOLFROOT)/wolfcrypt/src/wc_mlkem_poly.c endif +# aes.c + wc_encrypt.c are wanted by AES=1, AESEXTRA=1 and HWAES=1. Each sets +# NEED_AES_CORE and the pair is appended once below: the build is a single +# cl2000 invocation, so a source listed twice multiply-defines at link. +NEED_AES_CORE := 0 + # AES=1 adds AES-CBC/CTR/CFB/GCM (software, table-driven; GCM_SMALL GHASH). AES ?= 0 ifeq ($(AES),1) CFLAGS += --define=WOLF_AES - WC_SRCS += \ - $(WOLFROOT)/wolfcrypt/src/aes.c \ - $(WOLFROOT)/wolfcrypt/src/wc_encrypt.c + NEED_AES_CORE := 1 endif # X25519=1 adds Curve25519 (X25519) + Ed25519 (default fe[10] 32-bit backend). @@ -164,14 +170,24 @@ ifeq ($(AESEXTRA),1) CFLAGS += --define=WOLF_AES --define=WOLF_AESEXTRA WC_SRCS += \ $(WOLFROOT)/wolfcrypt/src/cmac.c - # aes.c and wc_encrypt.c are also pulled in by AES=1; add them here only when - # AES=1 did not, so they are not listed twice in the single cl2000 invocation - # (which would multiply-define their symbols at link). - ifneq ($(AES),1) - WC_SRCS += \ - $(WOLFROOT)/wolfcrypt/src/aes.c \ - $(WOLFROOT)/wolfcrypt/src/wc_encrypt.c - endif + NEED_AES_CORE := 1 +endif + +# HWAES=1 offloads AES-ECB/CBC/CTR to the on-chip AESA accelerator (TI EIP-120t +# at 0x42000) via crypto callbacks. Software AES stays compiled in so one image +# can compare both paths. Implies AES=1; driverlib.lib is already linked. +HWAES ?= 0 +ifeq ($(HWAES),1) + CFLAGS += --define=WOLF_AES --define=WOLF_HWAES + # WC_USE_DEVID points wolfcrypt_test and benchmark at the hardware device; + # without it they init every Aes context with INVALID_DEVID and silently + # measure/test software only. It is derived from WOLFSSL_C2000_DEVID in + # Header/user_settings.h rather than repeated as a literal here, so the two + # cannot drift apart. + WC_SRCS += \ + $(WOLFROOT)/wolfcrypt/src/cryptocb.c \ + $(WOLFROOT)/wolfcrypt/src/port/ti/ti-c2000-aes.c + NEED_AES_CORE := 1 endif # RSA=1 adds RSA verify (SP math backend, shared with the ECC P-256 build). @@ -261,6 +277,13 @@ ifeq ($(MEMPROF),1) CFLAGS += --define=WOLF_MEM_PROFILE endif +# Append the shared AES core once, after every toggle has had its say. +ifeq ($(NEED_AES_CORE),1) + WC_SRCS += \ + $(WOLFROOT)/wolfcrypt/src/aes.c \ + $(WOLFROOT)/wolfcrypt/src/wc_encrypt.c +endif + ALL_SRCS := $(WC_SRCS) $(HARNESS_SRCS) $(BSP_SRCS) $(ASM_SRCS) .PHONY: all clean diff --git a/embedded/ti-c2000-f28p55x/README.md b/embedded/ti-c2000-f28p55x/README.md index f642a38ad..cd623781e 100644 --- a/embedded/ti-c2000-f28p55x/README.md +++ b/embedded/ti-c2000-f28p55x/README.md @@ -10,7 +10,7 @@ The default build runs a KAT suite plus `wolfcrypt_test` and (optionally) `bench - SHA3-224/256/384/512, SHAKE128, SHAKE256 (split-64 Keccak permutation, ~53% faster than the generic C path on C28x) - ML-DSA-87 (Dilithium level 5) verify, and the full keygen+sign+verify round-trip (`SIGN=1`) - ML-KEM-768 (FIPS 203) keygen/encap/decap round-trip (`MLKEM=1`) -- AES-128/192/256 CBC/CTR/CFB/GCM (`AES=1`); AES-CMAC, AES-CCM, AES-GMAC (`AESEXTRA=1`) +- AES-128/192/256 CBC/CTR/CFB/GCM (`AES=1`); AES-CMAC, AES-CCM, AES-GMAC (`AESEXTRA=1`); hardware-accelerated AES-ECB/CBC/CTR on the on-chip AESA block (`HWAES=1`) - HMAC-SHA256 + HKDF (`HKDF=1`) - ChaCha20-Poly1305 AEAD + Poly1305 (`CHACHA=1`) - X25519 + Ed25519 (`X25519=1`) @@ -54,6 +54,7 @@ Each is `make =1` (default 0 unless noted), additive on top of the default | `MLKEM=1` | ML-KEM-768 (FIPS 203) | | `AES=1` | AES-CBC/CTR/CFB/GCM (table-driven, `GCM_SMALL`) | | `AESEXTRA=1` | AES-CMAC, AES-CCM, AES-GMAC (implies the AES core) | +| `HWAES=1` | Offload AES-ECB/CBC/CTR to the on-chip AESA accelerator via crypto callbacks (implies the AES core). See "Hardware AES" below | | `X25519=1` | Curve25519 (X25519) + Ed25519 | | `HKDF=1` | HMAC + HKDF (RFC 2104 / RFC 5869) | | `CHACHA=1` | ChaCha20-Poly1305 AEAD (RFC 8439) | @@ -100,6 +101,31 @@ ML-DSA-87 (asymmetric, @150 MHz): verify ~225 ms/op; keygen and signing also run - Big SP/`*_NO_MALLOC` structs (ecc_key, RsaKey, ChaChaPoly_Aead) belong in `.bss`/static, not on the stack: the SP point/modexp call tree plus a stack-allocated key can overflow the 16 KW stack. - `wc_RsaSSL_Verify` in the `RSA_VERIFY_ONLY` / `SP_NO_MALLOC` config runs the modexp in place in the caller's buffer, so the output buffer must be at least the key size (256 B for RSA-2048). +## Hardware AES (`HWAES=1`) + +The F28P550SJ has an on-chip AES accelerator ("AESA", a TI EIP-120t at `0x00042000`) that C2000Ware exposes through `driverlib/f28p55x/driverlib/aes.h`. `HWAES=1` offloads AES-ECB/CBC/CTR to it via the wolfCrypt crypto-callback framework (`wolfcrypt/src/port/ti/ti-c2000-aes.c` in the wolfSSL tree, gated on `WOLFSSL_C2000_AES`). `driverlib.lib` is already linked by this example, so no extra build plumbing is needed. + +Software AES stays compiled in. A context opts into hardware with `wc_AesInit(&aes, NULL, WOLFSSL_C2000_DEVID)`; one initialised with `INVALID_DEVID` runs pure software. `wolf_aes_hw_test()` uses both and compares them, which is the point: on a 16-bit-byte target the octet marshalling into the accelerator's 32-bit registers is the highest-risk part of the port, and a mismatch is exactly what you want to see. The harness prints 13 lines covering ECB/CBC/CTR at 128/192/256 bits, multi-block, split calls, in-place decrypt and a non-block-aligned CTR split, each checked against software and (for the first block of each mode) against the published NIST SP800-38A vector. + +`HWAES=1` also defines `WC_USE_DEVID=0x2000` so `wolfcrypt_test` and `benchmark` exercise the device too -- without it they init every context with `INVALID_DEVID` and silently measure software. + +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-CTR | 408 KiB/s | 1.45 MiB/s | 3.6x | +| AES-256-CTR | 335 KiB/s | 1.44 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 -The F28P55x has **no hardware TRNG**. The example uses `WOLFSSL_GENSEED_FORTEST` (random.c's built-in incrementing test seed feeding the real SHA-256 Hash-DRBG): 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**. 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()`. diff --git a/embedded/ti-c2000-f28p55x/Source/wolf_main.c b/embedded/ti-c2000-f28p55x/Source/wolf_main.c index 6c282de62..e1a893f61 100644 --- a/embedded/ti-c2000-f28p55x/Source/wolf_main.c +++ b/embedded/ti-c2000-f28p55x/Source/wolf_main.c @@ -55,6 +55,9 @@ #ifdef WOLF_AES #include #endif +#ifdef WOLF_HWAES +#include +#endif #ifdef WOLF_25519 #include #include @@ -1170,6 +1173,155 @@ static void wolf_aes_test(void) } #endif /* WOLF_AES */ +#ifdef WOLF_HWAES +/* Cross-check the AESA hardware against software AES. + * + * Two contexts deliberately: 'hw' carries WOLFSSL_C2000_DEVID so every aes.c + * hook routes to the callback, 'sw' carries INVALID_DEVID so every hook skips + * it. That separation matters -- with HAVE_AES_ECB on, a devId-bearing + * context would route even the software CTR path's internal wc_AesEcbEncrypt + * back to hardware. + * + * NIST SP800-38A vectors are asserted where we have them; multi-block, + * split-call and in-place cases are checked hardware-against-software, since + * software AES is already covered by wolfcrypt_test. */ +static void hw_report(const char* name, int r, const byte* a, const byte* b, + word32 len) +{ + printf("HW %s: %s\r\n", name, + (r == 0 && XMEMCMP(a, b, len) == 0) ? "PASS" : "FAIL"); +} + +static void wolf_aes_hw_test(void) +{ + /* NIST SP800-38A F.1/F.2/F.5 four-block plaintext. */ + static const byte pt[64] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96, + 0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a, + 0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c, + 0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51, + 0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11, + 0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xef, + 0xf6,0x9f,0x24,0x45,0xdf,0x4f,0x9b,0x17, + 0xad,0x2b,0x41,0x7b,0xe6,0x6c,0x37,0x10}; + static const byte k128[16] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6, + 0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c}; + static const byte k192[24] = { + 0x8e,0x73,0xb0,0xf7,0xda,0x0e,0x64,0x52, + 0xc8,0x10,0xf3,0x2b,0x80,0x90,0x79,0xe5, + 0x62,0xf8,0xea,0xd2,0x52,0x2c,0x6b,0x7b}; + static const byte k256[32] = { + 0x60,0x3d,0xeb,0x10,0x15,0xca,0x71,0xbe, + 0x2b,0x73,0xae,0xf0,0x85,0x7d,0x77,0x81, + 0x1f,0x35,0x2c,0x07,0x3b,0x61,0x08,0xd7, + 0x2d,0x98,0x10,0xa3,0x09,0x14,0xdf,0xf4}; + static const byte iv[16] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f}; + static const byte ctr_iv[16] = { + 0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7, + 0xf8,0xf9,0xfa,0xfb,0xfc,0xfd,0xfe,0xff}; + /* First-block published answers (F.1.1, F.2.1, F.5.1). */ + static const byte ecb_ct1[16] = { + 0x3a,0xd7,0x7b,0xb4,0x0d,0x7a,0x36,0x60, + 0xa8,0x9e,0xca,0xf3,0x24,0x66,0xef,0x97}; + static const byte cbc_ct1[16] = { + 0x76,0x49,0xab,0xac,0x81,0x19,0xb2,0x46, + 0xce,0xe9,0x8e,0x9b,0x12,0xe9,0x19,0x7d}; + /* Full 64-octet CTR answer, not just block 1: this vector's counter starts + * at ...fe ff, so block 2 is the first needing a carry across an octet + * boundary, and checking only block 1 hides a broken increment. */ + static const byte ctr_ct[64] = { + 0x87,0x4d,0x61,0x91,0xb6,0x20,0xe3,0x26, + 0x1b,0xef,0x68,0x64,0x99,0x0d,0xb6,0xce, + 0x98,0x06,0xf6,0x6b,0x79,0x70,0xfd,0xff, + 0x86,0x17,0x18,0x7b,0xb9,0xff,0xfd,0xff, + 0x5a,0xe4,0xdf,0x3e,0xdb,0xd5,0xd3,0x5e, + 0x5b,0x4f,0x09,0x02,0x0d,0xb0,0x3e,0xab, + 0x1e,0x03,0x1d,0xda,0x2f,0xbe,0x03,0xd1, + 0x79,0x21,0x70,0xa0,0xf3,0x00,0x9c,0xee}; + + /* .bss, not stack: the C28x stack is 16 KW and an Aes is not small. */ + static Aes hw, sw; + static byte oh[64], os[64], dh[64]; + int rh, rs; + + if (wc_AesInit(&hw, NULL, WOLFSSL_C2000_DEVID) != 0 || + wc_AesInit(&sw, NULL, INVALID_DEVID) != 0) { + printf("HW AES init: FAIL\r\n"); + return; + } + + /* ---- ECB, 64 octets (exercises the multi-block loop) ---- */ + rh = wc_AesSetKey(&hw, k128, 16, NULL, AES_ENCRYPTION); + rs = wc_AesSetKey(&sw, k128, 16, NULL, AES_ENCRYPTION); + if (rh == 0) rh = wc_AesEcbEncrypt(&hw, oh, pt, 64); + if (rs == 0) rs = wc_AesEcbEncrypt(&sw, os, pt, 64); + hw_report("AES-128-ECB encrypt vs NIST", rh, oh, ecb_ct1, 16); + hw_report("AES-128-ECB encrypt vs SW", (rh | rs), oh, os, 64); + + rh = wc_AesSetKey(&hw, k128, 16, NULL, AES_DECRYPTION); + if (rh == 0) rh = wc_AesEcbDecrypt(&hw, dh, oh, 64); + hw_report("AES-128-ECB decrypt round-trip", rh, dh, pt, 64); + + /* ---- CBC, 64 octets ---- */ + rh = wc_AesSetKey(&hw, k128, 16, iv, AES_ENCRYPTION); + rs = wc_AesSetKey(&sw, k128, 16, iv, AES_ENCRYPTION); + if (rh == 0) rh = wc_AesCbcEncrypt(&hw, oh, pt, 64); + if (rs == 0) rs = wc_AesCbcEncrypt(&sw, os, pt, 64); + hw_report("AES-128-CBC encrypt vs NIST", rh, oh, cbc_ct1, 16); + hw_report("AES-128-CBC encrypt vs SW", (rh | rs), oh, os, 64); + + rh = wc_AesSetKey(&hw, k128, 16, iv, AES_DECRYPTION); + if (rh == 0) rh = wc_AesCbcDecrypt(&hw, dh, oh, 64); + hw_report("AES-128-CBC decrypt round-trip", rh, dh, pt, 64); + + /* ---- CBC split across calls: proves aes->reg chaining ---- */ + rh = wc_AesSetKey(&hw, k128, 16, iv, AES_ENCRYPTION); + if (rh == 0) rh = wc_AesCbcEncrypt(&hw, oh, pt, 16); + if (rh == 0) rh = wc_AesCbcEncrypt(&hw, oh + 16, pt + 16, 48); + hw_report("AES-128-CBC split-call chain", (rh | rs), oh, os, 64); + + /* ---- CBC in-place decrypt: proves the last-block save ---- */ + XMEMCPY(dh, os, 64); + rh = wc_AesSetKey(&hw, k128, 16, iv, AES_DECRYPTION); + if (rh == 0) rh = wc_AesCbcDecrypt(&hw, dh, dh, 64); + hw_report("AES-128-CBC in-place decrypt", rh, dh, pt, 64); + + /* ---- CTR, 64 octets ---- */ + rh = wc_AesSetKey(&hw, k128, 16, ctr_iv, AES_ENCRYPTION); + rs = wc_AesSetKey(&sw, k128, 16, ctr_iv, AES_ENCRYPTION); + if (rh == 0) rh = wc_AesCtrEncrypt(&hw, oh, pt, 64); + if (rs == 0) rs = wc_AesCtrEncrypt(&sw, os, pt, 64); + hw_report("AES-128-CTR vs NIST (64B)", rh, oh, ctr_ct, 64); + hw_report("AES-128-CTR SW vs NIST (64B)", rs, os, ctr_ct, 64); + hw_report("AES-128-CTR vs SW", (rh | rs), oh, os, 64); + + /* ---- CTR split at a non-block boundary: proves aes->left/aes->tmp ---- */ + rh = wc_AesSetKey(&hw, k128, 16, ctr_iv, AES_ENCRYPTION); + if (rh == 0) rh = wc_AesCtrEncrypt(&hw, oh, pt, 10); + if (rh == 0) rh = wc_AesCtrEncrypt(&hw, oh + 10, pt + 10, 54); + hw_report("AES-128-CTR partial split", (rh | rs), oh, os, 64); + + /* ---- 192- and 256-bit keys: the 6- and 8-word AES_setKey1 paths ---- */ + rh = wc_AesSetKey(&hw, k192, 24, iv, AES_ENCRYPTION); + rs = wc_AesSetKey(&sw, k192, 24, iv, AES_ENCRYPTION); + if (rh == 0) rh = wc_AesCbcEncrypt(&hw, oh, pt, 64); + if (rs == 0) rs = wc_AesCbcEncrypt(&sw, os, pt, 64); + hw_report("AES-192-CBC encrypt vs SW", (rh | rs), oh, os, 64); + + rh = wc_AesSetKey(&hw, k256, 32, iv, AES_ENCRYPTION); + rs = wc_AesSetKey(&sw, k256, 32, iv, AES_ENCRYPTION); + if (rh == 0) rh = wc_AesCbcEncrypt(&hw, oh, pt, 64); + if (rs == 0) rs = wc_AesCbcEncrypt(&sw, os, pt, 64); + hw_report("AES-256-CBC encrypt vs SW", (rh | rs), oh, os, 64); + + wc_AesFree(&hw); + wc_AesFree(&sw); +} +#endif /* WOLF_HWAES */ + #ifdef WOLF_25519 static void wolf_curve25519_test(void) { @@ -1747,11 +1899,27 @@ int main(void) printf("\r\n"); printf("=== wolfSSL wolfCrypt on TI C2000 LAUNCHXL-F28P55X ===\r\n"); + #ifdef WOLF_MEM_PROFILE /* Route XMALLOC/XFREE/XREALLOC through the heap high-water tracker. */ wolf_mem_install(); #endif +#ifdef WOLF_HWAES + /* wolfCrypt_Init() is mandatory first: it sets every device-table slot to + * INVALID_DEVID, and RegisterDevice only claims a slot marked that way. + * Without it the table is BSS-zero and registration fails with BUFFER_E. */ + if (wolfCrypt_Init() != 0) { + printf("wolfCrypt_Init: FAIL\r\n"); + } + else if (wc_C2000_Init(WOLFSSL_C2000_DEVID) != 0) { + printf("C2000 AESA init: FAIL\r\n"); + } + else { + printf("C2000 AESA init: PASS\r\n"); + } +#endif + wolf_sha3_256_test(); wolf_sha256_test(); wolf_shake256_test(); @@ -1796,6 +1964,11 @@ int main(void) wolf_aes_test(); #endif /* WOLF_AES */ +#ifdef WOLF_HWAES + printf("\r\n--- AES hardware (AESA) vs software ---\r\n"); + wolf_aes_hw_test(); +#endif /* WOLF_HWAES */ + #ifdef WOLF_25519 printf("\r\n--- Curve25519 (X25519) + Ed25519 ---\r\n"); wolf_curve25519_test(); From a4c09c7f40d4124e6fb9d90522a4bc0b955d1cac Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 13 Aug 2026 09:45:52 -0700 Subject: [PATCH 2/2] TI C2000 example: real entropy source, probe image and validation KATs --- .../ti-c2000-f28p55x/Header/user_settings.h | 26 +- embedded/ti-c2000-f28p55x/Makefile | 21 +- embedded/ti-c2000-f28p55x/README.md | 44 ++- .../ti-c2000-f28p55x/Source/entropy_probe.c | 252 +++++++++++++++ embedded/ti-c2000-f28p55x/Source/wolf_main.c | 81 +++++ .../ti-c2000-f28p55x/tools/entropy_analyze.py | 305 ++++++++++++++++++ 6 files changed, 712 insertions(+), 17 deletions(-) create mode 100644 embedded/ti-c2000-f28p55x/Source/entropy_probe.c create mode 100755 embedded/ti-c2000-f28p55x/tools/entropy_analyze.py diff --git a/embedded/ti-c2000-f28p55x/Header/user_settings.h b/embedded/ti-c2000-f28p55x/Header/user_settings.h index 13389c6b5..b3ba57417 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 ca2a1afbf..ac5289e69 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 cd623781e..bdcfe8e0d 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 000000000..2554f5389 --- /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 e1a893f61..51e383c26 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 000000000..65493aa39 --- /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()