TI C2000 example: SECUREBOOT=1 pure-mode ML-DSA verify of a packed image streamed from flash
parent
1238312e9c
commit
fd1f216859
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue