TI C2000 example: SECUREBOOT=1 pure-mode ML-DSA verify of a packed image streamed from flash

pull/617/head
David Garske 2026-09-02 09:42:51 -07:00
parent 1238312e9c
commit fd1f216859
5 changed files with 1319 additions and 4 deletions

File diff suppressed because it is too large Load Diff

View File

@ -122,6 +122,14 @@ ifeq ($(FASTVERIFY),1)
CFLAGS += --define=WOLF_MLDSA_FAST_VERIFY
endif
# SECUREBOOT=1 verifies a PACKED firmware image straight from flash in pure
# (non pre-hash) ML-DSA mode, streaming it through SHAKE-256 so the image is
# never resident. Standalone image, like MLDSA=1.
SECUREBOOT ?= 0
ifeq ($(SECUREBOOT),1)
CFLAGS += --define=WOLF_SECUREBOOT --define=WOLF_MLDSA_ALL_LEVELS
endif
MLDSA ?= 0
ifeq ($(MLDSA),1)
CFLAGS += --define=WOLF_MLDSA_ALL_LEVELS
@ -279,6 +287,10 @@ ifeq ($(SIGN),1)
--define=NO_CRYPT_BENCHMARK
LNKCMD := $(CURDIR)/28p55x_wolf_sign_lnk.cmd
HARNESS_SRCS :=
else ifeq ($(SECUREBOOT),1)
CFLAGS += --define=NO_CRYPT_TEST --define=NO_CRYPT_BENCHMARK
LNKCMD := $(CURDIR)/28p55x_wolf_flash_lnk.cmd
HARNESS_SRCS :=
else ifeq ($(MLDSA),1)
ifeq ($(BENCH),1)
$(error MLDSA=1 and BENCH=1 are separate images - build them one at a time)

View File

@ -26,6 +26,16 @@ Data arriving from flash, SCI, CAN or a host tool is **packed**, two octets per
`make MLDSA=1` proves this on hardware: ML-DSA-44/65/87 verify, `wc_MlDsaKey_VerifyCtxHash()` over SHA-256/SHA-512 with a non-empty context, and a verify from a packed key and signature. Vectors are in `Header/mldsa_octet_kat.h`, regenerated by `tools/gen_kat.c` (build it against a host wolfSSL, run it, and move the header into `Header/`). Full write-up: `IDE/C2000/README.md` in the wolfSSL tree.
## Pure-mode secure boot (`SECUREBOOT=1`)
CNSA 2.0 prefers pure ML-DSA (no pre-hash), but `wc_MlDsaKey_VerifyCtx()` takes the whole message as one buffer -- impossible for a firmware image on a part with 133 KB of RAM, doubled to 2 bytes per octet.
It does not need to be buffered. The message reaches ML-DSA only through `mu = SHAKE256(tr || 0x00 || ctxLen || ctx || M)` where `tr = SHAKE256(publicKey)`, and a hash streams. `wc_MlDsaKey_VerifyMu()` takes `mu` directly -- this is ExternalMu-ML-DSA, the same mode ACVP tests -- so the bootloader reads the image a chunk at a time, expands each chunk with `wc_UnpackOctets()`, absorbs it, and verifies. RAM cost is the chunk buffer plus 128 octets, whatever the image size, and the result is bit-identical to `VerifyCtx()`.
`make SECUREBOOT=1` proves it on hardware: an 8192-octet image stored packed in flash, streamed in 256-octet chunks, plus a corrupted-image rejection. The same loop scales to any image size.
Three things that fail *silently* if they do not match your signer: the `0x00 || ctxLen` prefix must be present (the legacy no-context API omits it), `tr` is over the **raw** encoded key rather than a DER/SPKI wrapper, and the SHAKE context must be your own -- wolfSSL reuses the one inside the key during verification.
## The 16-bit-byte (CHAR_BIT == 16) story
On the C28x a `char`/`unsigned char` (wolfSSL's `byte`) holds 16 bits, the minimum addressable unit is 16 bits, and `(byte)x` masks to 16 bits, not 8. wolfCrypt octet handling that assumed an 8-bit byte was made `CHAR_BIT`-agnostic behind the `WOLFSSL_WIDE_BYTE` gate (auto-enabled for `CHAR_BIT != 8` and known 16-bit-char TI toolchains), with zero change on 8-bit targets. The fixes fall into a few recurring classes:
@ -71,7 +81,9 @@ Each is `make <NAME>=1` (default 0 unless noted), additive on top of the default
| `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) |
| `SECUREBOOT=1` | Pure-mode (non pre-hash) ML-DSA-87 verify of a PACKED firmware image streamed straight from flash -- the image is never held in RAM. See "Pure-mode secure boot" below |
| `MLDSA=1` | ML-DSA octet-boundary KATs: verify at all three parameter sets, `wc_MlDsaKey_VerifyCtxHash()` over SHA-256/SHA-512, and a verify from a PACKED key/signature via `wc_UnpackOctets()`. No test/bench harness. See "Octet representation" below |
| `FASTVERIFY=1` | Trade RAM for ML-DSA verify speed: keeps the full `z` vector and uses a 64-bit accumulator. Measured 244 -> 198 ms/op for ML-DSA-87 verify, RAM 10.7 -> 23.9 KB |
| `BENCH=1` | Run only `benchmark` instead of `wolfcrypt_test` (they need separate images on this RAM-limited part) |
Other knobs: `CGT_ROOT` (required), `C2000WARE`, `WOLFROOT`, and `CIO=1` (route `printf` to the CCS/JTAG console via CIO instead of the default SCI/UART).
@ -104,7 +116,7 @@ Measured at 150 MHz (SHA-256 Hash-DRBG via `WOLFSSL_GENSEED_FORTEST`; build with
| SHAKE128 / SHAKE256 | ~319 / 264 KiB/s |
| RNG (Hash-DRBG) | ~122 KiB/s |
ML-DSA-87 (asymmetric, @150 MHz): verify ~225 ms/op; keygen and signing also run (`SIGN=1`). ML-DSA-87 verify memory: ~10.7 KB total (key/workspace struct + ~2 KB stack, zero heap) with `WOLFSSL_MLDSA_VERIFY_SMALLEST_MEM` + `WOLFSSL_MLDSA_ASSIGN_KEY`.
ML-DSA-87 (asymmetric, @150 MHz): verify 244 ms/op default, **189 ms/op** with `FASTVERIFY=1` and `--opt_level=3 --opt_for_speed=5` (23% faster, RAM 10.7 -> 23.9 KB, zero heap either way); keygen and signing also run (`SIGN=1`). ML-DSA-87 verify memory: ~10.7 KB total (key/workspace struct + ~2 KB stack, zero heap) with `WOLFSSL_MLDSA_VERIFY_SMALLEST_MEM` + `WOLFSSL_MLDSA_ASSIGN_KEY`.
## Memory notes

View File

@ -103,7 +103,7 @@
#endif
#include <wolfcrypt/test/test.h>
#include <wolfcrypt/benchmark/benchmark.h>
#ifdef WOLF_MLDSA_OCTETS
#if defined(WOLF_MLDSA_OCTETS) || defined(WOLF_SECUREBOOT)
/* The octet-boundary image carries its own seed-derived vectors for all three
* parameter sets (it needs the matching private key to produce the pre-hash
* signatures), and reuses the same kat_mldsa87_* names. The canonical FIPS 204
@ -1188,6 +1188,120 @@ static void wolf_mldsa_octet_test(void)
}
#endif /* WOLF_MLDSA_OCTETS */
#ifdef WOLF_SECUREBOOT
/* ------------------------------------------------------------------------- */
/* Pure-mode ML-DSA secure boot (make SECUREBOOT=1) */
/* ------------------------------------------------------------------------- */
/* Verifies a firmware image that is stored PACKED in flash - two octets per
* 16-bit cell, the layout a host signing tool and the C28x programmer produce -
* without ever holding the image in RAM.
*
* ML-DSA has no streaming interface, but the message reaches the algorithm only
* through mu = SHAKE256(tr || 0x00 || ctxLen || ctx || M), and a hash streams.
* wc_MlDsaKey_VerifyMu() takes mu directly (ExternalMu-ML-DSA), so the image is
* read a chunk at a time, expanded with wc_UnpackOctets(), and absorbed.
* RAM cost is the chunk buffer plus 128 octets, whatever the image size. */
#define SB_CHUNK 256 /* octets per flash read */
static wc_MlDsaKey sb_key; /* .bss: too big for the 16 KW C28x stack */
/* Build mu over the packed image. flip < 0 leaves the image intact; otherwise
* one octet is corrupted, to prove a bad image is rejected. */
static int sb_build_mu(byte* mu, long flip)
{
wc_Shake sh;
byte tr[MLDSA_TR_SZ];
byte buf[SB_CHUNK];
byte prefix[2];
word32 off;
int ret;
/* tr = SHAKE256(raw public key). Constant for a fixed verification key,
* so a production bootloader would precompute this at build time. */
ret = wc_InitShake256(&sh, NULL, INVALID_DEVID);
if (ret == 0) {
ret = wc_Shake256_Update(&sh, sb_mldsa87_pub,
(word32)sizeof(sb_mldsa87_pub));
}
if (ret == 0) {
ret = wc_Shake256_Final(&sh, tr, (word32)sizeof(tr));
}
wc_Shake256_Free(&sh);
if (ret != 0) {
return ret;
}
/* mu = SHAKE256(tr || 0x00 || ctxLen || ctx || image). 0x00 selects pure
* (non pre-hash) mode; the context here is empty. */
prefix[0] = 0x00;
prefix[1] = 0x00;
ret = wc_InitShake256(&sh, NULL, INVALID_DEVID);
if (ret == 0) {
ret = wc_Shake256_Update(&sh, tr, (word32)sizeof(tr));
}
if (ret == 0) {
ret = wc_Shake256_Update(&sh, prefix, 2);
}
for (off = 0; (ret == 0) && (off < SB_IMG_SZ); off += SB_CHUNK) {
word32 n = SB_IMG_SZ - off;
if (n > SB_CHUNK) {
n = SB_CHUNK;
}
ret = wc_UnpackOctets(buf, (word32)sizeof(buf),
(const byte*)sb_image_packed + (off / WC_OCTETS_PER_BYTE),
WC_PACKED_CELLS(n), n);
if ((ret == 0) && (flip >= 0) &&
((word32)flip >= off) && ((word32)flip < off + n)) {
buf[(word32)flip - off] ^= 0x01;
}
if (ret == 0) {
ret = wc_Shake256_Update(&sh, buf, n);
}
}
if (ret == 0) {
ret = wc_Shake256_Final(&sh, mu, MLDSA_MU_SZ);
}
wc_Shake256_Free(&sh);
return ret;
}
static void sb_verify(const char* what, long flip, int want)
{
byte mu[MLDSA_MU_SZ];
int res = -1;
int ret;
ret = sb_build_mu(mu, flip);
if (ret == 0) {
ret = wc_MlDsaKey_Init(&sb_key, NULL, INVALID_DEVID);
}
if (ret == 0) {
ret = wc_MlDsaKey_SetParams(&sb_key, WC_ML_DSA_87);
}
if (ret == 0) {
ret = wc_MlDsaKey_ImportPubRaw(&sb_key, sb_mldsa87_pub,
(word32)sizeof(sb_mldsa87_pub));
}
if (ret == 0) {
ret = wc_MlDsaKey_VerifyMu(&sb_key, sb_mldsa87_sig,
(word32)sizeof(sb_mldsa87_sig), mu, MLDSA_MU_SZ, &res);
}
printf("%s %s (ret=%d res=%d)\r\n", what,
((ret == 0) && (res == want)) ? "PASS" : "FAIL", ret, res);
wc_MlDsaKey_Free(&sb_key);
}
static void wolf_secureboot_test(void)
{
printf("secure boot: %lu-octet packed image, %u-octet chunks, "
"image never resident\r\n",
(unsigned long)SB_IMG_SZ, (unsigned)SB_CHUNK);
sb_verify("ML-DSA-87 pure-mode packed-image verify:", -1, 1);
sb_verify("ML-DSA-87 corrupted-image reject:", SB_IMG_SZ / 2, 0);
}
#endif /* WOLF_SECUREBOOT */
#ifdef WOLF_MLDSA_SIGN
/* ML-DSA-87 sign+verify round-trip (keygen -> sign -> verify). Exercises
* the full signer on the C28x. Uses the DEV RNG stub (NOT secure) - this
@ -2303,7 +2417,10 @@ int main(void)
wolf_sha1_test();
#endif
#if defined(WOLF_MLDSA_OCTETS)
#ifdef WOLF_SECUREBOOT
printf("\r\n--- Pure-mode ML-DSA secure boot ---\r\n");
wolf_secureboot_test();
#elif defined(WOLF_MLDSA_OCTETS)
printf("\r\n--- ML-DSA octet boundary (44/65/87, pre-hash, packed) ---\r\n");
wolf_mldsa_octet_test();
#elif !defined(WOLF_MLDSA_SIGN)

View File

@ -36,6 +36,11 @@
#define MSG_SZ 512
#define OUT_NAME "mldsa_octet_kat.h"
/* A stand-in firmware image for the secure-boot demo. Deliberately larger
* than anything a bootloader would buffer, to show the image is never
* resident: it is stored PACKED in flash and streamed through SHAKE-256. */
#define IMG_SZ 8192
static const byte kSeed[MLDSA_SEED_SZ] = {
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
@ -155,6 +160,43 @@ static int do_level(int type, const char* tag, const byte* msg,
return ret;
}
/* Secure-boot vectors: an ML-DSA-87 key, a packed "firmware image", and a
* PURE (non pre-hash) signature over that image. */
static int do_secureboot(byte* img)
{
wc_MlDsaKey key;
byte pub[MLDSA_MAX_PUB_KEY_SIZE];
byte sig[MLDSA_MAX_SIG_SIZE];
word32 pubLen = (word32)sizeof(pub);
word32 sigLen = (word32)sizeof(sig);
int ret;
ret = wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID);
if (ret == 0)
ret = wc_MlDsaKey_SetParams(&key, WC_ML_DSA_87);
if (ret == 0)
ret = wc_MlDsaKey_MakeKeyFromSeed(&key, kSeed);
if (ret == 0)
ret = wc_MlDsaKey_ExportPubRaw(&key, pub, &pubLen);
if (ret == 0) {
/* Pure mode: sign the image itself, no pre-hash, empty context. */
ret = wc_MlDsaKey_SignCtxWithSeed(&key, NULL, 0, sig, &sigLen,
img, IMG_SZ, kRnd);
}
if (ret == 0) {
fprintf(out, "\n#define SB_IMG_SZ %u\n\n", (unsigned)IMG_SZ);
emit_bytes("sb_mldsa87_pub", pub, pubLen);
emit_packed("sb_image_packed", img, IMG_SZ);
emit_bytes("sb_mldsa87_sig", sig, sigLen);
}
else {
fprintf(stderr, "secure-boot vector generation failed: %d\n", ret);
}
wc_MlDsaKey_Free(&key);
return ret;
}
int main(void)
{
byte msg[MSG_SZ];
@ -200,7 +242,16 @@ int main(void)
"#define MLDSA_OCTET_KAT_H\n\n"
"#define KAT_MLDSA_CTX \"%s\"\n\n", kCtx);
ret = do_level(WC_ML_DSA_44, "44", msg, sha256, sha512, 0, 0);
{ /* deterministic stand-in image */
static byte img[IMG_SZ];
int j;
for (j = 0; j < IMG_SZ; j++) {
img[j] = (byte)(((j * 31) ^ (j >> 5)) & 0xFF);
}
ret = do_secureboot(img);
}
if (ret == 0)
ret = do_level(WC_ML_DSA_44, "44", msg, sha256, sha512, 0, 0);
if (ret == 0)
ret = do_level(WC_ML_DSA_65, "65", msg, sha256, sha512, 1, 0);
if (ret == 0)