wolfCrypt on the TI C2000 C28x: PQC ML-DSA/ML-KEM on a 16-bit-byte DSP

We’re excited to share that wolfCrypt now runs a full modern crypto suite on Texas Instruments’ C2000 C28x – the NIST post-quantum schemes ML-DSA (FIPS 204) and ML-KEM (FIPS 203), plus RSA, Diffie-Hellman, AES, ChaCha20-Poly1305, HMAC/HKDF, the Curve25519/Ed25519, Curve448/Ed448, and NIST P-256 elliptic-curve schemes, and the SHA-1/2/3 and SHAKE hash families – all validated on the LAUNCHXL-F28P55X (TMS320F28P550SJ) real-time MCU. The full per-algorithm breakdown, including key sizes and modes, is in “What runs today” below.

Why this one was interesting

The C28x is a real-time control DSP found across motor control, digital power, and industrial systems. It is also one of the most unusual targets a crypto library can meet: its smallest addressable unit is 16 bits. There is no 8-bit type at all – char, short, and int are all 16-bit, CHAR_BIT == 16, and sizeof(word32) is 2, not 4.

Almost every byte-oriented idiom in a crypto library quietly assumes an 8-bit byte: aliasing a 32-bit word as four bytes, using sizeof as a byte count, masking with a (byte) cast, computing a bit length as 8 * sizeof, or writing 1U << n for a shift past 16. On the C28x, every one of those is wrong - a 32-bit word is stored as two cells with two octets packed in each, while a byte buffer holds one octet per cell. Getting the hashes, the Hash-DRBG, the symmetric ciphers and AEADs, ML-KEM, ML-DSA, X25519/Ed25519, X448/Ed448, and the RSA/ECC single-precision math correct meant making the octet/word boundary explicit wherever data crosses it - all behind a single WOLFSSL_WIDE_BYTE build gate that is a no-op on every 8-bit-byte platform.

What runs today

On the F28P55x at 150 MHz, validated on hardware with known-answer tests:

  • Hashing: SHA-1, SHA-256/384/512 (+512-224, 512-256), SHA3-224/256/384/512, SHAKE128/256.
  • Symmetric / AEAD / MAC: AES-128/192/256 in CBC, CTR, CFB, OFB, GCM, XTS; AES-CMAC, AES-CCM, AES-GMAC, AES-SIV, AES-EAX; ChaCha20-Poly1305 and Poly1305; HMAC and HKDF.
  • Post-quantum: ML-DSA-44/65/87 verify, key generation, and signing; ML-KEM-512/768/1024 key generation, encapsulation, and decapsulation (byte-exact against a host reference).
  • Classic public key: RSA-2048 PKCS#1 v1.5 sign and verify, DH (FFDHE-2048), ECDSA/ECDH P-256, X25519 (RFC 7748), Ed25519 (RFC 8032), and X448/Ed448 (RFC 7748 / RFC 8032).
  • RNG: the real SHA-256 Hash-DRBG.

Performance

Throughput of the generic C implementation at 150 MHz:

Algorithm Throughput
SHA-256 ~284 KiB/s
SHA-384 / SHA-512 / SHA-512-224 / SHA-512-256 ~166 KiB/s
SHA3-224 / SHA3-256 / SHA3-384 / SHA3-512 279 / 264 / 206 / 146 KiB/s
SHAKE128 / SHAKE256 319 / 264 KiB/s
RNG (SHA-256 Hash-DRBG) ~122 KiB/s
ML-DSA-87 verify ~225 ms/op

Footprint

Code and RAM measured on the F28P55x (in octets):

Item Size
ML-DSA-87 signature / private key 4627 B / 7488 B
ML-KEM-1024 ciphertext / public+private key 1568 B / 4736 B
ML-DSA sign+verify code ~22 KB
ML-KEM make+enc+dec code ~16 KB
SHA-3/SHAKE code (small / fast split-64) ~5 / 16 KB
ML-DSA-87 verify RAM ~10.8 KB (zero heap)
ML-KEM-1024 make / encap / decap RAM ~2 / 6 / 9 KB

Making SHA-3 (and post-quantum) faster, in portable C

SHAKE is the workhorse underneath both ML-DSA and ML-KEM, so the Keccak permutation is worth tuning. Comparing the compiler's output against a hand-written assembly, we found the gap was not the algorithm, it was that the C compiler was emitting an out-of-line helper call for every 64-bit XOR, OR and AND (about 870 calls per permutation). The fix is pure, portable C: run the permutation on 32-bit halves so the compiler emits native 32-bit instructions and keeps the column-parity accumulators in registers. The result is ~53% faster SHAKE/SHA3 (SHAKE128 from 211 to 319 KiB/s), and because ML-DSA-87 verify is SHAKE-bound it dropped about 26%, from ~305 to ~225 ms/op - no assembly, and gated to 16-bit-word targets so no other platform changes.

Squeezing post-quantum verify into ~10 KiB of RAM

Constrained targets care most about RAM, and ML-DSA-87 is a large scheme. We added a streaming verify mode that processes the signature's response vector one polynomial at a time instead of holding all of it, and - paired with keeping the public key in flash by reference - brought ML-DSA-87 verify down to about 10.8 KiB of total RAM (from roughly 22-25 KiB), with zero heap and an under-2.5-KiB stack high-water.

Upstream-first and portable

Every change sits behind a single build switch, so platforms with the usual 8-bit byte are completely unaffected and run exactly as before. Better still, a few of the fixes are wins for everyone, not just this DSP: a couple were long-standing corner cases that simply never showed up where a byte is the expected size, and they are now corrected for all targets. And an automated check that needs no hardware rebuilds the affected code with the TI compiler on every change, so this support keeps working as wolfCrypt grows.

Try it

The core WOLFSSL_WIDE_BYTE support is in wolfSSL PR #10724, and the bare-metal example is in wolfssl-examples PR #576.

The example - board bring-up, linker scripts, known-answer tests, and a test/benchmark harness, with per-algorithm build toggles for AES (and CMAC/CCM/GMAC plus XTS/OFB/SIV/EAX), ChaCha20-Poly1305, HKDF, ML-KEM, X25519, X448, ECC, RSA, DH, and the full ML-DSA sign demo - lives in the wolfssl-examples repository under embedded/ti-c2000-f28p55x/. It builds with the free TI C2000 code generation tools.

If you have questions about any of the above, please contact us at facts@wolfssl.com or call us at +1 425 245 8247.

Download wolfSSL Now