Fox Red Labs / Benchmarks / Security
Benchmarks
Exchange a key. Send an encrypted message.
Post-quantum key exchange and signatures on the LaunchPad boards: what ran and what it costs.
A Mac and a CC1354P10-1 LaunchPad board agree a key, then the board decrypts a message sent over USB. The key exchange is ML-KEM-768, a post-quantum algorithm. The message is AES-128-GCM. The key check line shows the first 4 bytes of a hash of the agreed key. They match, so both sides hold the same key.
board> join: key pair ready, offering the public key key check Mac d01a2f61 board d01a2f61 MATCH board> R,pqc,join_decrypt,119689,ok,AES-128-GCM, 76 bytes board> DECRYPTED (76 bytes, tag ok): Hello from the Mac: this line was encrypted with a key agreed by ML-KEM-768.
Excerpt of the saved result from 2026-10-08 on the CC1354P10-1, 4 of its lines, unedited.
Build this demo Demonstration only. The exchange does not authenticate either endpoint: nothing proves who is on the other end.
Build this demo: the board and what you need
Board: CC1354P10-1 LaunchPad development kit. It is the only board that has run the whole path: 296 kB of RAM and a Cortex-M33 at 48 MHz. One USB cable.
| Need | Detail |
|---|---|
| Board | CC1354P10-1 LaunchPad development kit with a CC1354P10, a Cortex-M33 at 48 MHz. One USB cable to the Mac. |
| Board tools | CCStudio 21.0.1, TI Arm Clang 5.1.1.LTS and the SimpleLink CC13xx/CC26xx SDK 8.33.00.16 (NoRTOS), for the build and the load. |
| Repository | The cc1354p10/pqc-join folder of the repository. |
| Mac | A Mac. The scripts use macOS paths and CCStudio's command-line scripting; the run was on macOS 15.7 (Intel). |
| Host tools | A C compiler, Python 3 with the cryptography package, and OpenSSL 3.5 or newer first on your PATH. Install the package with pip install cryptography. The Mac's built-in /usr/bin/openssl is LibreSSL and cannot do ML-KEM, so check openssl version. The run used OpenSSL 3.6.4. The host test needs only these, no board. |
| USB hub | A USB hub whose ports uhubctl can power-switch, because this board's debugger connects only just after power-on. Setup or handling. run.sh cuts power to the port for 6 s just before each load, as cc1354p10/FAQ.md describes. On the test setup that was the USB-A port of Apple's USB-C Digital AV Multiport Adapter, as cc1354p10/FAQ.md describes. No manual unplug and replug was done in place of the hub. |
| Your board's name | run.sh and the CCStudio target file cc1354p10/CC1354P10.ccxml name the test board by its debug-probe serial number. Edit both for yours. |
| Flash is rewritten | run.sh backs up the board's flash and boot settings, loads the demo, then restores the image named in RESTORE_IMG and reads flash back. Name an image you want left on the board before you run it. |
Run it and what you should see
- Run the host test. It needs no board; it builds the board code for the Mac and runs both known-answer sets and the whole exchange against mac_side.py.
portability/pqc-join/test/run.shYou should see:
pqc-join host test: OK - Build the board image. It touches no hardware. It took 19.5 s on 2026-10-08 with the SysConfig output already generated, and longer the first time.
cc1354p10/pqc-join/build.sh -O3You should see:
a .out file; 272,999 B for the whole -O3 image - Load and run it. run.sh power-cycles the USB port, backs up flash and the boot-settings page, and loads the image. It runs mac_side.py on the UART, then restores the image you name and reads flash back. Replace the UHUB value with your hub and port from uhubctl.
UHUB=<hub:port from uhubctl> RESTORE_IMG=<path to the .out to leave running> cc1354p10/pqc-join/run.sh -O3You should see:
the transcript above, then READBACK MATCHES BACKUP (2 minutes 22 seconds on 2026-10-08)If it stops: If the board stops at "join: making an ML-KEM-768 key pair" with the CPU in an exception handler, call TRNG_init() before TRNG_open(). Stop with a message if TRNG_open() returns NULL. The debugger showed PC 0x100006f4 and LR 0xfffffff9. This was observed on 2026-10-08 and not saved. The SDK's Board_init() starts only the power and GPIO drivers, so the image has to start the TRNG itself; cc1354p10/pqc-join/README.md, the TRNG section, has the fix.
Full write-up, with every step timed and the files listed: LP-CC1354P10-1 PQC join notes. The shared code is in portability/pqc-join.
The whole run, as saved
board> I,results,kat pass board> join: making an ML-KEM-768 key pair board> R,pqc,join_trng_64_bytes,967756,ok,64 random bytes for the key pair board> R,pqc,join_keygen,1002199,ok,ML-KEM-768 key pair from the random bytes board> join: key pair ready, offering the public key board> R,pqc,join_decaps_kdf,1430503,ok,decapsulate, HKDF-SHA256 to the AES key, key check key check Mac d01a2f61 board d01a2f61 MATCH board> R,pqc,join_decrypt,119689,ok,AES-128-GCM, 76 bytes board> DECRYPTED (76 bytes, tag ok): Hello from the Mac: this line was encrypted with a key agreed by ML-KEM-768.
Scroll sideways inside the box to read the long lines.
Source: cc1354p10/pqc-join/results/load4-join.txt. Lines 54 to 62 of the saved result file, unedited. Board lines start with board>. The key check line is the Mac's. The hex frames are not in this file; the exchange as JSON is in cc1354p10/pqc-join/results/load4-join.json.
What this shows
- The two sides derived the same AES-128 key: the key check is d01a2f61 on the Mac and on the board, and the line reads MATCH.
- The board decrypted one message and its tag verified under the agreed key. Neither side is authenticated, so anyone in the middle of the exchange would hold that key too. This does not say who sent it.
- NIST known-answer vectors for ML-KEM-768 and ML-DSA-44 passed on the board before the exchange. See the "kat pass" line in the same file.
What it leaves open
- No protection against power-analysis or fault attacks is claimed by the libraries or by the demo code.
- It is a demonstration of the math and its cost, not a hardened product. Do not use the keys for anything real, and do not reuse them.
- It ran once over one wired transport, with no radio link. The join has no signature step, so neither side is authenticated. It has not been reviewed or tested for side channels.
- The known-answer vectors check the arithmetic against NIST's fixed inputs. They do not show the code is safe to ship.
What this means for your project
- Key agreement fits in about 20 kB of stack on a part with room to spare. On this 296 kB board, ML-KEM-768 peaked at 14.7 kB of stack for key generation, 20 kB for encapsulation and 18.7 kB for decapsulation. That is mlkem-native in portable C at -O3. ML-DSA-44 signing peaked at 50.1 kB. A 32 kB board should fit key agreement but was not tested. Signing there needs mldsa-native's low-RAM mode.
- The wire set the pace. The board's own work for the whole join was about 73 ms at 48 MHz. The wire took about 420 ms at 115,200 baud (computed, not measured). That time is long because the host and board exchange the keys, ciphertext and message as hex text, which doubles the bytes on the wire. A radio would add its own cost.
- A product needs signatures and a review. This join has no signature step, so neither side is authenticated. The code has had no side-channel or fault testing, review or certification. A product would sign or pin the keys, test for those attacks and have the design reviewed.
More: flash, wire size and integration
- Flash. Flash at -O3 is 80.5 kB for ML-KEM-768 and 106.7 kB for ML-DSA-44. Built for size (-Os, built, not run) it is 9.4 kB and 13.3 kB.
- Wire size. 2.2 kB crosses the wire (1.2 kB public key plus 1.1 kB ciphertext) against 64 B for X25519 (32 B each way). Over a radio with small packets that is many more packets: about 10 Bluetooth LE packets of 244 B against 2 (computed, not measured).
- Integration. Initialize the TRNG driver yourself; the SDK's Board_init() does not. The libraries (mlkem-native 2.0.0, mldsa-native 2.0.0) are unmodified portable C and keep their working buffers on the stack by default. ML-DSA signing time varies from message to message (77 to 361 ms over 16 messages, mean 154 ms) because it retries until a check passes.
Which boards can do it
Post-quantum cryptography is key exchange and signatures built to survive a large quantum computer. Such a computer could break today's RSA and elliptic curves. NIST, the U.S. standards body, standardized ML-KEM for key exchange and ML-DSA for signatures in 2024. TI publishes ML-KEM-768 key generation, encapsulation and decapsulation at 27, 31 and 36 ms on the MSPM0G3519 at 80 MHz and 11, 12 and 14 ms on the MSPM33C321A at 160 MHz. Those times use its MSP Crypto Library.
Key agreement (ML-KEM-768)
- Works CC1354P10-1: the demonstration above, about 20 kB of stack.
- Not for this chip MSPM0 C1104: one 1.2 kB key exceeds its 1 kB of RAM.
Signing (ML-DSA-44)
- Works CC1354P10-1: ML-DSA-44 signing measured with 50 kB of stack, in an image with a 64 kB stack.
Measured by us
1 kB = 1,024 bytes. Cycles from the CPU's own counter, converted to milliseconds at the clock shown; peak stack from painting the stack before each call. Rows come from data/results/.
| Board | Operation | Implementation | Result | Peak stack | Source |
|---|---|---|---|---|---|
| CC1354P10-1 | ML-KEM-768 key generation | mlkem-native, TI Arm Clang, -O3, code in flash | 20.8 ms at 48 MHz | 14.7 kB | cc1354p10/pqc-join/README.md |
| CC1354P10-1 | ML-KEM-768 encapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 24.0 ms at 48 MHz | 20 kB | cc1354p10/pqc-join/README.md |
| CC1354P10-1 | ML-KEM-768 decapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 29.1 ms at 48 MHz | 18.7 kB | cc1354p10/pqc-join/README.md |
| CC1354P10-1 | Code in flash, ML-KEM-768 at -O3 | mlkem-native, TI Arm Clang, -O3 | 80.5 kB | cc1354p10/pqc-join/README.md | |
| CC1354P10-1 | Code in flash, ML-KEM-768 at -Os, built not run | mlkem-native, TI Arm Clang, -Os | 9.4 kB | cc1354p10/pqc-join/README.md | |
| CC1354P10-1 | ML-DSA-44 key generation | mldsa-native, TI Arm Clang, -O3, code in flash | 49.5 ms at 48 MHz | 32 kB | cc1354p10/pqc-join/README.md |
| CC1354P10-1 | ML-DSA-44 sign | mldsa-native, TI Arm Clang, -O3, code in flash | 154.3 ms at 48 MHz, mean | 50.1 kB | cc1354p10/pqc-join/README.md |
| CC1354P10-1 | ML-DSA-44 verify | mldsa-native, TI Arm Clang, -O3, code in flash | 50.5 ms at 48 MHz | 30.2 kB | cc1354p10/pqc-join/README.md |
| CC1354P10-1 | Code in flash, ML-DSA-44 at -O3 | mldsa-native, TI Arm Clang, -O3 | 106.7 kB | cc1354p10/pqc-join/README.md | |
| CC1354P10-1 | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, TI Arm Clang, -Os | 13.3 kB | cc1354p10/pqc-join/README.md | |
| CC2755P10 | Code in flash, ML-KEM-768 at -O3 | mlkem-native, TI Arm Clang, -O3 | 80.5 kB | ||
| CC2755P10 | Code in flash, ML-DSA-44 at -O3 | mldsa-native, TI Arm Clang, -O3 | 106.7 kB | ||
| CC2755P10 | Code in flash, ML-KEM-768 at -Os, built not run | mlkem-native, TI Arm Clang, -Os | 9.4 kB | ||
| CC2755P10 | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, TI Arm Clang, -Os | 13.3 kB | ||
| CC2755P10 | ML-KEM-768 key generation | mlkem-native, TI Arm Clang, -O3, code in flash | 10.0 ms at 96 MHz | 13.7 kB | |
| CC2755P10 | ML-KEM-768 encapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 11.6 ms at 96 MHz | 16.8 kB | |
| CC2755P10 | ML-KEM-768 decapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 14.1 ms at 96 MHz | 17.7 kB | |
| CC2755P10 | ML-DSA-44 key generation | mldsa-native, TI Arm Clang, -O3, code in flash | 23.9 ms at 96 MHz | 31.3 kB | |
| CC2755P10 | ML-DSA-44 sign | mldsa-native, TI Arm Clang, -O3, code in flash | 49.7 ms at 96 MHz, mean | 48.3 kB | |
| CC2755P10 | ML-DSA-44 verify | mldsa-native, TI Arm Clang, -O3, code in flash | 24.3 ms at 96 MHz | 29.2 kB | |
| CC2755P10 | ML-KEM-768 key generation | mlkem-native, TI Arm Clang, -Os, code in flash | 10.3 ms at 96 MHz | 13.7 kB | |
| CC2755P10 | ML-KEM-768 encapsulation | mlkem-native, TI Arm Clang, -Os, code in flash | 11.9 ms at 96 MHz | 16.8 kB | |
| CC2755P10 | ML-KEM-768 decapsulation | mlkem-native, TI Arm Clang, -Os, code in flash | 14.6 ms at 96 MHz | 17.7 kB | |
| CC2755P10 | ML-DSA-44 key generation | mldsa-native, TI Arm Clang, -Os, code in flash | 23.4 ms at 96 MHz | 31.3 kB | |
| CC2755P10 | ML-DSA-44 sign | mldsa-native, TI Arm Clang, -Os, code in flash | 47.3 ms at 96 MHz, mean | 48.3 kB | |
| CC2755P10 | ML-DSA-44 verify | mldsa-native, TI Arm Clang, -Os, code in flash | 23.7 ms at 96 MHz | 29.2 kB | |
| CC2755P10 | ECDSA P-256 sign | micro-ecc, plain C, TI Arm Clang, -O2 | 115.1 ms at 96 MHz | ||
| CC2755P10 | ECDSA P-256 verify | micro-ecc, plain C, TI Arm Clang, -O2 | 131.4 ms at 96 MHz | ||
| CC2755P10 | ECDSA P-256 sign | micro-ecc, assembly, TI Arm Clang, -O2 | 81.2 ms at 96 MHz | ||
| CC2755P10 | ECDSA P-256 verify | micro-ecc, assembly, TI Arm Clang, -O2 | 94.1 ms at 96 MHz | ||
| CC2755P10 | ECDSA P-256 sign | TI library, driver, hsm, TI Arm Clang, -O2 | 24.3 ms at 96 MHz | ||
| CC2755P10 | ECDSA P-256 verify | TI library, driver, hsm, TI Arm Clang, -O2 | 41.7 ms at 96 MHz | ||
| MSPM0 G3519 | ML-KEM-768 key generation | mlkem-native, TI Arm Clang, -O3, code in flash | 24.6 ms at 80 MHz | 14.6 kB | portability/boards/mspm0g3519/PORT.md |
| MSPM0 G3519 | ML-KEM-768 encapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 29.1 ms at 80 MHz | 20 kB | portability/boards/mspm0g3519/PORT.md |
| MSPM0 G3519 | ML-KEM-768 decapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 35.6 ms at 80 MHz | 18.8 kB | portability/boards/mspm0g3519/PORT.md |
| MSPM0 G3519 | ML-DSA-44 key generation | mldsa-native, TI Arm Clang, -O3, code in flash | 82.2 ms at 80 MHz | 14.7 kB | portability/boards/mspm0g3519/PORT.md |
| MSPM0 G3519 | ML-DSA-44 sign | mldsa-native, TI Arm Clang, -O3, code in flash | 317.4 ms at 80 MHz | 17.2 kB | portability/boards/mspm0g3519/PORT.md |
| MSPM0 G3519 | ML-DSA-44 verify | mldsa-native, TI Arm Clang, -O3, code in flash | 95.5 ms at 80 MHz | 11.4 kB | portability/boards/mspm0g3519/PORT.md |
| MSPM0 G3519 | ML-DSA-44 sign | mldsa-native, TI Arm Clang, -O3, code in flash, 16 messages | 515.7 ms at 80 MHz, mean | 17.2 kB | portability/boards/mspm0g3519/PORT.md |
| MSPM0 G3519 | Code in flash, ML-KEM-768 at -O3 | mlkem-native, TI Arm Clang, -O3 | 88.5 kB | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | Code in flash, ML-KEM-768 at -Os, built not run | mlkem-native, TI Arm Clang, -Os | 11.2 kB | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | Code in flash, ML-DSA-44 at -O3 | mldsa-native, TI Arm Clang, -O3 | 192.2 kB | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, TI Arm Clang, -Os | 16.2 kB | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | ECDSA P-256 sign | TI library, example, software, TI Arm Clang, -O2 | 417.3 ms at 80 MHz | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | ECDSA P-256 verify | TI library, example, software, TI Arm Clang, -O2 | 749.8 ms at 80 MHz | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | ECDSA P-256 sign | micro-ecc, assembly, TI Arm Clang, -O2 | 208.1 ms at 80 MHz | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | ECDSA P-256 verify | micro-ecc, assembly, TI Arm Clang, -O2 | 233.1 ms at 80 MHz | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | ECDSA P-256 sign | micro-ecc, plain C, TI Arm Clang, -O2 | 477.0 ms at 80 MHz | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | ECDSA P-256 verify | micro-ecc, plain C, TI Arm Clang, -O2 | 528.8 ms at 80 MHz | portability/boards/mspm0g3519/PORT.md | |
| MSPM0 G3519 | Code in flash, ML-DSA-44 at -O3 | mldsa-native, low-RAM, TI Arm Clang, -O3 | 115.8 kB | cc1354p10/pqc-join/README.md | |
| MSPM0 G3519 | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, low-RAM, TI Arm Clang, -Os | 15.1 kB | cc1354p10/pqc-join/README.md | |
| MSPM0 G3519 | ML-DSA-44 key generation | mldsa-native, low-RAM, TI Arm Clang, -O3, code in flash | 84.0 ms at 80 MHz | 14.7 kB | portability/pqc-join/lowram_build.sh |
| MSPM0 G3519 | ML-DSA-44 sign | mldsa-native, low-RAM, TI Arm Clang, -O3, code in flash | 308.8 ms at 80 MHz, mean | 17.2 kB | portability/pqc-join/lowram_build.sh |
| MSPM0 G3519 | ML-DSA-44 verify | mldsa-native, low-RAM, TI Arm Clang, -O3, code in flash | 97.4 ms at 80 MHz | 11.4 kB | portability/pqc-join/lowram_build.sh |
| MSPM0 G5187 | ECDSA P-256 sign | TI library, TI Arm Clang, -O2, code in flash | 417.3 ms at 80 MHz | ||
| MSPM0 G5187 | ECDSA P-256 verify | TI library, TI Arm Clang, -O2, code in flash | 749.9 ms at 80 MHz | ||
| MSPM0 G5187 | ECDSA P-256 sign | micro-ecc, assembly, TI Arm Clang, -O2, code in flash | 208.1 ms at 80 MHz | ||
| MSPM0 G5187 | ECDSA P-256 verify | micro-ecc, assembly, TI Arm Clang, -O2, code in flash | 233.1 ms at 80 MHz | ||
| MSPM0 G5187 | ECDSA P-256 sign | micro-ecc, plain C, TI Arm Clang, -O2, code in flash | 482.8 ms at 80 MHz | ||
| MSPM0 G5187 | ECDSA P-256 verify | micro-ecc, plain C, TI Arm Clang, -O2, code in flash | 535.3 ms at 80 MHz | ||
| MSPM0 G5187 | ML-KEM-768 key generation | mlkem-native, TI Arm Clang, -Os, code in flash | 24.9 ms at 80 MHz | 13.8 kB | |
| MSPM0 G5187 | ML-KEM-768 encapsulation | mlkem-native, TI Arm Clang, -Os, code in flash | 29.8 ms at 80 MHz | 16.8 kB | |
| MSPM0 G5187 | ML-KEM-768 decapsulation | mlkem-native, TI Arm Clang, -Os, code in flash | 36.4 ms at 80 MHz | 17.7 kB | |
| MSPM0 G5187 | Code in flash, ML-KEM-768 at -Os, built not run | mlkem-native, TI Arm Clang, -Os | 11.2 kB | ||
| MSPM0 G5187 | Code in flash, ML-KEM-768 at -O3 | mlkem-native, TI Arm Clang, -O3 | 88.5 kB | ||
| MSPM0 G5187 | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, TI Arm Clang, -Os | 16.2 kB | ||
| MSPM0 G5187 | Code in flash, ML-DSA-44 at -O3 | mldsa-native, low-RAM, TI Arm Clang, -O3 | 115.8 kB | cc1354p10/pqc-join/README.md | |
| MSPM0 G5187 | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, low-RAM, TI Arm Clang, -Os | 15.1 kB | cc1354p10/pqc-join/README.md | |
| MSPM0 G5187 | ML-DSA-44 key generation | mldsa-native, low-RAM, TI Arm Clang, -Os, code in flash | 83.6 ms at 80 MHz | 12.2 kB | |
| MSPM0 G5187 | ML-DSA-44 verify | mldsa-native, low-RAM, TI Arm Clang, -Os, code in flash | 99.3 ms at 80 MHz | 10.9 kB | |
| MSPM33 C321A | ML-KEM-768 key generation | TI library, example, TI Arm Clang, -O2, code in flash | 46.3 ms at 32 MHz | 13.7 kB | mspm33c321a/tools/build_crypto.sh |
| MSPM33 C321A | ML-KEM-768 encapsulation | TI library, example, TI Arm Clang, -O2, code in flash | 53.7 ms at 32 MHz | 16.8 kB | mspm33c321a/tools/build_crypto.sh |
| MSPM33 C321A | ML-KEM-768 decapsulation | TI library, example, TI Arm Clang, -O2, code in flash | 62.1 ms at 32 MHz | 17.7 kB | mspm33c321a/tools/build_crypto.sh |
| MSPM33 C321A | ML-DSA-44 key generation | TI library, example, TI Arm Clang, -O2, code in flash | 85.4 ms at 32 MHz | 14.6 kB | mspm33c321a/tools/build_crypto.sh |
| MSPM33 C321A | ML-DSA-44 sign | TI library, example, TI Arm Clang, -O2, code in flash | 724.7 ms at 32 MHz | 16.9 kB | mspm33c321a/tools/build_crypto.sh |
| MSPM33 C321A | ML-DSA-44 verify | TI library, example, TI Arm Clang, -O2, code in flash | 108.5 ms at 32 MHz | 11.4 kB | mspm33c321a/tools/build_crypto.sh |
| MSPM33 C321A | ECDSA P-256 sign | TI library, example, TI Arm Clang, -O2, code in flash | 162.9 ms at 32 MHz | mspm33c321a/tools/build_crypto.sh | |
| MSPM33 C321A | ECDSA P-256 verify | TI library, example, TI Arm Clang, -O2, code in flash | 328.2 ms at 32 MHz | mspm33c321a/tools/build_crypto.sh | |
| MSPM33 C321A | Code in flash, ML-KEM-768 at -O3 | mlkem-native, TI Arm Clang, -O3 | 80.5 kB | ||
| MSPM33 C321A | Code in flash, ML-KEM-768 at -Os, built not run | mlkem-native, TI Arm Clang, -Os | 9.4 kB | ||
| MSPM33 C321A | Code in flash, ML-DSA-44 at -O3 | mldsa-native, TI Arm Clang, -O3 | 106.7 kB | ||
| MSPM33 C321A | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, TI Arm Clang, -Os | 13.3 kB | ||
| MSPM33 C321A | ML-KEM-768 key generation | mlkem-native, TI Arm Clang, -O3, code in flash | 8.1 ms at 160 MHz | 13.7 kB | |
| MSPM33 C321A | ML-KEM-768 encapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 9.2 ms at 160 MHz | 16.8 kB | |
| MSPM33 C321A | ML-KEM-768 decapsulation | mlkem-native, TI Arm Clang, -O3, code in flash | 11.3 ms at 160 MHz | 17.7 kB | |
| MSPM33 C321A | ML-DSA-44 key generation | mldsa-native, TI Arm Clang, -O3, code in flash | 19.1 ms at 160 MHz | 14.6 kB | |
| MSPM33 C321A | ML-DSA-44 verify | mldsa-native, TI Arm Clang, -O3, code in flash | 19.5 ms at 160 MHz | 11.4 kB | |
| MSPM33 C321A | ML-KEM-768 key generation | mlkem-native, TI Arm Clang, -Os, code in flash | 8.3 ms at 160 MHz | 13.7 kB | |
| MSPM33 C321A | ML-KEM-768 encapsulation | mlkem-native, TI Arm Clang, -Os, code in flash | 9.4 ms at 160 MHz | 16.8 kB | |
| MSPM33 C321A | ML-KEM-768 decapsulation | mlkem-native, TI Arm Clang, -Os, code in flash | 11.5 ms at 160 MHz | 17.7 kB | |
| MSPM33 C321A | ML-DSA-44 key generation | mldsa-native, TI Arm Clang, -Os, code in flash | 18.9 ms at 160 MHz | 14.6 kB | |
| MSPM33 C321A | ML-DSA-44 verify | mldsa-native, TI Arm Clang, -Os, code in flash | 19.0 ms at 160 MHz | 11.4 kB | |
| MSPM33 C321A | ML-DSA-44 sign | mldsa-native, TI Arm Clang, -O3, code in flash | 58.1 ms at 160 MHz, mean | 16.9 kB | |
| MSPM33 C321A | ML-DSA-44 sign | mldsa-native, TI Arm Clang, -Os, code in flash | 55.4 ms at 160 MHz, mean | 16.9 kB | |
| MSPM33 C321A | Code in flash, ML-DSA-44 at -O3 | mldsa-native, low-RAM, TI Arm Clang, -O3 | 88.1 kB | cc1354p10/pqc-join/README.md | |
| MSPM33 C321A | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, low-RAM, TI Arm Clang, -Os | 12.7 kB | cc1354p10/pqc-join/README.md | |
| MSPM33 C321A | ML-DSA-44 key generation | mldsa-native, low-RAM, TI Arm Clang, -O3, code in flash | 19.4 ms at 160 MHz | 14.6 kB | |
| MSPM33 C321A | ML-DSA-44 sign | mldsa-native, low-RAM, TI Arm Clang, -O3, code in flash | 54.4 ms at 160 MHz, mean | 16.9 kB | |
| MSPM33 C321A | ML-DSA-44 verify | mldsa-native, low-RAM, TI Arm Clang, -O3, code in flash | 19.6 ms at 160 MHz | 11.4 kB | |
| NUCLEO-U083RC | Code in flash, ML-KEM-768 at -O3 | mlkem-native, GCC 14, -O3 | 20.5 kB | ||
| NUCLEO-U083RC | Code in flash, ML-KEM-768 at -Os, built not run | mlkem-native, GCC 14, -Os | 9.9 kB | ||
| NUCLEO-U083RC | Code in flash, ML-DSA-44 at -O3 | mldsa-native, GCC 14, -O3 | 32.3 kB | ||
| NUCLEO-U083RC | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, GCC 14, -Os | 13.8 kB | ||
| NUCLEO-U083RC | Code in flash, ML-DSA-44 at -O3 | mldsa-native, low-RAM, GCC 14, -O3 | 29.4 kB | portability/pqc-join/boards/nucleo-u083rc/build.sh -O3 dsa-lowram | |
| NUCLEO-U083RC | Code in flash, ML-DSA-44 at -Os, built not run | mldsa-native, low-RAM, GCC 14, -Os | 13.1 kB | portability/pqc-join/boards/nucleo-u083rc/build.sh -Os dsa-lowram | |
| SK-TDA4VM | ML-KEM-768 key generation | mlkem-native, GCC, -O3, a72 | 0.1 ms at 2000 MHz | ||
| SK-TDA4VM | ML-KEM-768 encapsulation | mlkem-native, GCC, -O3, a72 | 0.1 ms at 2000 MHz | ||
| SK-TDA4VM | ML-KEM-768 decapsulation | mlkem-native, GCC, -O3, a72 | 0.1 ms at 2000 MHz | ||
| SK-TDA4VM | ML-DSA-44 key generation | mldsa-native, GCC, -O3, a72 | 0.2 ms at 2000 MHz | ||
| SK-TDA4VM | ML-DSA-44 sign | mldsa-native, GCC, -O3, a72 | 0.6 ms at 2000 MHz | ||
| SK-TDA4VM | ML-DSA-44 verify | mldsa-native, GCC, -O3, a72 | 0.2 ms at 2000 MHz |
The exchange, step by step
ML-KEM-768 key agreement between the board and a Mac, then one line the Mac encrypted with AES-128-GCM under the agreed key, decrypted and authenticated on the board.
Transport. USB serial through the board's built-in XDS110 debugger (its backchannel UART), 115,200 baud, 8N1. Every byte is sent as two hex characters, so 2.3 kB of payload in all (the 2.2 kB of key material plus the encrypted message) took 4,864 characters on the wire. Frame format: @PK, @CT, @KEY, @MSG, @DONE lines, each with a CRC-32 (portability/pqc-join/core/pqc_join.h). The Mac side is tools/pqc_join/mac_side.py with OpenSSL 3.5 or newer (the Mac's built-in /usr/bin/openssl is LibreSSL and cannot do ML-KEM).
- The board makes an ML-KEM-768 key pair from 64 bytes of its hardware random-number generator (20 ms for the bytes, 21 ms for the key pair); the 2.3 kB private key stays on the chip
- The board sends its 1.2 kB public key (2,389 characters as hex, about 207 ms at 115,200 baud, computed)
- The Mac wraps a fresh secret with that key (58.6 ms, including starting OpenSSL) and sends back the 1.1 kB ciphertext
- The board unwraps it and turns the secret into an AES-128 key with HKDF-SHA-256 (29.8 ms on the board); both sides show the first 4 bytes of a hash of that key, and they match
- The Mac sends one 76-byte line encrypted with AES-128-GCM; the board decrypts it in software (2.5 ms), checks the tag and prints it
Board by board: estimated and measured
One row per board, one cell per operation. RAM needs differ by code. About 14 kB is the peak stack measured for PQClean on the RP2040 (arXiv 2603.19340). About 19 kB is an estimate for mlkem-native: its published 14 kB plus the 2.3 kB, 1.2 kB and 1.1 kB keys and ciphertext. Cells without a measured row are estimated from cycle counts published for other cores. Each cell cites its source.
Not for this chip the chip does not have the RAM for itWorks measured on a board; the detail is in the table above. Every other cell is estimated.
What the hardware blocks buy
Chips with an AES block run it far faster than software: the MSPM33 C321A runs AES 41.7 to 41.9 times faster at 1 kB. A block's win is largest on large data and shrinks on a single 16-byte block. The CRC block gains a few times, because the CPU still feeds it word by word. Each figure is hardware cycles against a software library on the same chip, from data/results/. A row range means the rows differ by key-setup handling or code variant.
| Board | AES on 1 kB, hardware over software | CRC on 1 kB, hardware over software |
|---|---|---|
| AM13E230 | 29.8 to 29.9 times | 1.89 times |
| CC1354P10-1 | 45.8 to 51.1 times | no measured row |
| CC2340R5 | 128.2 to 131.5 times | no measured row |
| ESP32-C6-DevKitC-1-N8 | 0.83 times | no measured row |
| FRDM-MCXA153 | no measured row | 7.06 times |
| MSPM0 C1104 | no measured row | 3 times |
| MSPM0 G3507 | 20.3 times | 13.5 times |
| MSPM0 G3519 | 45.8 times | 8.09 times |
| MSPM0 G5187 | 45.5 times | 8.12 times |
| MSPM0 L1306 | no measured row | 10.9 times |
| MSPM33 C321A | 41.7 to 41.9 times | 2.75 times |
| NUCLEO-U083RC | 19.6 to 93.9 times | 8.34 times |
| Raspberry Pi Pico 2 H | no measured row | 5.63 to 5.71 times |
Published by others, not measured by us
In TI's tables a post-quantum signature check (ML-DSA-44 verify, 98 ms) is faster on the MSPM0G3519's Cortex-M0+ than the classical one it replaces done in software (ECDSA P-256 verify, 716 ms). The cost is size: keys and ciphertexts of 800 to 4.5 kB instead of 32 to 64, and tens of kilobytes of RAM.
Published figures for comparable cores
Published figures for these operations on comparable cores, as each source states it. Different code, compilers and clocks, so compare cycles within one source before comparing across sources.
| Published by | Chip | Clock | Code | Operation | Result | Source |
|---|---|---|---|---|---|---|
| TI | MSPM0G3519 (Cortex-M0+) | 80 MHz | MSP Crypto Library 0.33.00.07 | ML-KEM-768 keygen / encaps / decaps | 2.16M / 2.51M / 2.86M cycles (27 / 31 / 36 ms) | MSPM0 SDK 2.11.00.07, MSP Crypto Library 0.33.00.07 user guide, Performance and Throughput |
| TI | MSPM0G3519 (Cortex-M0+) | 80 MHz | MSP Crypto Library 0.33.00.07 | ML-DSA-44 keygen / sign (mean of 16) / verify | 6.69M / 33.9M / 7.83M cycles (84 / 423 / 98 ms) | MSPM0 SDK 2.11.00.07, MSP Crypto Library 0.33.00.07 user guide, Performance and Throughput |
| TI | MSPM0G3519 (Cortex-M0+) | 80 MHz | MSP Crypto Library 0.33.00.07, software | ECDSA P-256 sign / verify | 31.4M / 57.3M cycles (392 / 716 ms) | MSPM0 SDK 2.11.00.07, MSP Crypto Library 0.33.00.07 user guide, Performance and Throughput |
| TI | MSPM33C321A (Cortex-M33) | 160 MHz | MSP Crypto Library 0.33.00.07 | ML-KEM-768 keygen / encaps / decaps | 1.76M / 1.96M / 2.22M cycles (11 / 12 / 14 ms) | MSPM0 SDK 2.11.00.07, MSP Crypto Library 0.33.00.07 user guide, Performance and Throughput |
| TI | MSPM33C321A (Cortex-M33) | 160 MHz | MSP Crypto Library 0.33.00.07 | ML-DSA-44 keygen / sign (mean of 16) / verify | 3.58M / 21.3M / 3.66M cycles (22 / 133 / 23 ms) | MSPM0 SDK 2.11.00.07, MSP Crypto Library 0.33.00.07 user guide, Performance and Throughput |
| TI | MSPM33C321A (Cortex-M33) | 160 MHz | MSP Crypto Library 0.33.00.07, PKA | ECDSA P-256 sign / verify | 10.4M / 21M cycles (65 / 131 ms) | MSPM0 SDK 2.11.00.07, MSP Crypto Library 0.33.00.07 user guide, Performance and Throughput |
| pqm4 project | STMicroelectronics STM32L4R5 (Cortex-M4) | 24 MHz | reference C (clean) | ML-KEM-768 keygen / encaps / decaps | 989k / 1.14M / 1.39M cycles | github.com |
| pqm4 project | STMicroelectronics STM32L4R5 (Cortex-M4) | 24 MHz | Cortex-M4 assembly (m4fspeed) | ML-KEM-768 keygen / encaps / decaps | 642k / 659k / 708k cycles | github.com |
| Chhetri, arXiv 2603.19340 | Raspberry Pi RP2040 (Cortex-M0+) | 133 MHz | PQClean reference C, GCC -Os | ML-KEM-768 keygen / encaps / decaps | 2.13M / 2.47M / 2.93M cycles (16.0 / 18.6 / 22.0 ms) | arxiv.org |
| Chhetri, arXiv 2603.19340 | Raspberry Pi RP2040 (Cortex-M0+) | 133 MHz | PQClean reference C, GCC -Os | ML-DSA-44 keygen / sign (mean) / verify | 39.8 / 158.9 / 44.0 ms (21.1 million cycles to sign; sign time varies 66 to 73 percent between runs) | arxiv.org |
| wolfSSL | STMicroelectronics STM32F439 (Cortex-M4) | 168 MHz | wolfCrypt with Thumb-2 assembly | ML-KEM-768 keygen / encaps / decaps | 6.5 / 6.6 / 8.3 ms | wolfssl.com |
| Karcz et al., PLOS One 2026 | Espressif ESP32-C6 (RISC-V) | 160 MHz | liboqs on ESP-IDF 5.5.1 | ML-KEM-768 keygen / encaps / decaps | 171 / 171 / 205 ms | journals.plos.org |
Why a chip buyer cares now
The dates that set the deadlines
From the standards bodies and governments themselves. NIST is the U.S. standards body; FIPS numbers are its standards; CNSA 2.0 is the U.S. National Security Agency's list of required algorithms. A device sold in 2027 may still be in the field after 2035.
- 2024-04-11: European Commission Recommendation on a coordinated roadmap for the move to post-quantum cryptography. digital-strategy.ec.europa.eu
- 2024-08-13: NIST publishes the final FIPS 203 (ML-KEM), FIPS 204 (ML-DSA) and FIPS 205 (SLH-DSA), ready for immediate use. nist.gov
- 2024-11-12: NIST IR 8547 draft: 112-bit ECDSA, ECDH and RSA deprecated after 2030; all quantum-vulnerable public-key algorithms, P-256 included, disallowed after 2035 (still a draft). csrc.nist.gov
- 2024-12: NSA CNSA 2.0 FAQ version 2.1: ML-KEM-1024 and ML-DSA-87 for U.S. national security systems. media.defense.gov
- 2025-06-23: EU member states' roadmap: start by the end of 2026, high-risk uses by the end of 2030 with quantum-safe firmware upgrades on by default, everything by the end of 2035. digital-strategy.ec.europa.eu
- 2027: NSA: from 2027 all new commercial national security systems must support quantum-resistant algorithms; legacy systems that cannot are phased out by 2030. nsa.gov
- 2030 to 2033: CNSA 2.0: firmware and software signing exclusively post-quantum by 2030; constrained devices support and prefer by 2030, exclusively by 2033. media.defense.gov
The two algorithms, and the sizes they bring
Key, ciphertext and signature sizes
ML-KEM replaces the key exchange, ML-DSA the signature. The last two rows are the classical algorithms they replace, for scale. "NIST category" is NIST's strength scale from 1 to 5: higher is stronger, and bigger.
| Algorithm | What it is | Public key | Ciphertext or signature | Source |
|---|---|---|---|---|
| ML-KEM-512 | Post-quantum key exchange, NIST category 1 (the lowest of five strength levels) | 800 | 768 | nvlpubs.nist.gov |
| ML-KEM-768 | Post-quantum key exchange, NIST category 3; NIST's recommended default | 1.2 kB | 1.1 kB | nvlpubs.nist.gov |
| ML-KEM-1024 | Post-quantum key exchange, NIST category 5; the size CNSA 2.0 requires | 1.5 kB | 1.5 kB | nvlpubs.nist.gov |
| ML-DSA-44 | Post-quantum signature, NIST category 2 | 1.3 kB | 2.4 kB | nvlpubs.nist.gov |
| ML-DSA-65 | Post-quantum signature, NIST category 3 | 1.9 kB | 3.2 kB | nvlpubs.nist.gov |
| ML-DSA-87 | Post-quantum signature, NIST category 5; the size CNSA 2.0 requires | 2.5 kB | 4.5 kB | nvlpubs.nist.gov |
| X25519 | Classical key exchange it replaces (the other side's public key is the reply) | 32 | 32 | MSPM0 SDK 2.11.00.07 source/ti/crypto/include/curve25519/curve25519.h (X25519_KEY_LENGTH) |
| ECDSA P-256 | Classical signature it replaces (public key as x and y coordinates) | 64 | 64 | MSPM0 SDK 2.11.00.07 source/ti/crypto/include/ec/ec.h (32-byte coordinates and r, s) |
Questions about this section
- Why does the board stop at "join: making an ML-KEM-768 key pair"?
- Call TRNG_init() before TRNG_open(), and stop with a message if TRNG_open() returns NULL. An image that skips TRNG_init() stops here; the SDK's Board_init() starts only the power and GPIO drivers. The TRNG is the chip's hardware random-number generator, which the key pair needs. The debugger showed the CPU in an exception handler, PC 0x100006f4 and LR 0xfffffff9 (observed 2026-10-08, not saved). cc1354p10/pqc-join/README.md, the TRNG section.
- Can someone in the middle read the message?
- Someone who only listens cannot. They see the public key and the ciphertext, and ML-KEM-768 is built so that does not give away the key. Someone who can change bytes on the wire can. They answer the board with their own key and the Mac with theirs, and each side agrees a key with them. Do not ship it as is: the demonstration has no signature step. A product signs or pins the keys, for example with ML-DSA-44, whose signing is measured above.
- What does the key check line mean, and what would MISMATCH mean?
- Each side prints the first 4 bytes of a hash of the key it holds (tools/pqc_join/mac_side.py). MATCH means the two are equal, so both sides almost certainly hold the same key, and the decryption tag passing confirms it. MISMATCH would mean they derived different keys and the board's decryption would fail its tag check. Every saved run printed MATCH; a MISMATCH has not been seen here.
- Why does the demo need a USB hub the Mac can switch?
- This board's debugger connects only in the first seconds after power-on, so run.sh switches the port off for 6 s and on again before each load. Install uhubctl (brew install uhubctl) and run it alone to list hubs; one marked ppps can switch its ports. Here that was the USB-A port of Apple's USB-C Digital AV Multiport Adapter. No manual unplug and replug was done. cc1354p10/FAQ.md.
- Are any of these numbers measured by Fox Red Labs?
- Some are. Measured by us: 111 results on 7 boards (CC1354P10-1, CC2755P10, MSPM0 G3519, MSPM0 G5187, MSPM33 C321A, NUCLEO-U083RC, SK-TDA4VM), in the Measured by us table above, with 47 peak-stack measurements shown beside them. The other times are TI's published figures or estimates. Every cell marked Not run is TI's published figure or an estimated range, and the "published by others" table is exactly that.
- Why can the MSPM0 L1306 and MSPM0 C1104 not do post-quantum key exchange?
- RAM. The smallest portable ML-KEM-768 found needs about 14 kB of working RAM (PQClean on the RP2040, arXiv 2603.19340); the MSPM0 L1306 has 4 kB and the MSPM0 C1104 has 1 kB, less than one 1.2 kB public key. The answer is no for the MSPM0 C1104 (its RAM is below the size of one public key). For the MSPM0 L1306 the answer is expected to be no: it comes from published RAM needs, not from a build.
- Why does the CC2340R5 fit ML-KEM alone but not beside Bluetooth?
- ML-KEM-768 with mlkem-native needs about 19 kB of working RAM (an estimate). TI's basic Bluetooth example leaves 5.1 kB of the chip's 36 kB. So room is expected only without the Bluetooth stack. That is one TI example configuration; a trimmed Bluetooth build or a lower-RAM ML-KEM was not built.
- Is post-quantum slower than what it replaces?
- In TI's own tables, not for checking signatures: ML-DSA-44 verify is faster than ECDSA P-256 verify on both chips TI lists. Signing is slower. Every key, ciphertext and signature is 20 to 72 times bigger, which costs more on a radio link.
- Why do ML-DSA signing times differ so much between TI's cores and runs?
- Signing repeats an internal try until a check passes, so its time varies from run to run. The RP2040 paper saw 66 to 73 percent spread, and TI averages only 16 signs. TI's own ML-DSA-44 sign mean is 40.4M cycles at 32 MHz but 33.9M cycles at 80 MHz on the same chip (TI's MSP Crypto Library 0.33.00.07 user guide, MSPM0 SDK 2.11.00.07, performance tables). The difference is probably run-to-run noise (suspected, not repeated). Verification time is fixed. TI's runs were not repeated here.
- Why does the MSPM0 G3507 fit ML-DSA signing only in low-RAM mode?
- The default mode of mldsa-native needs 43.4 kB for signing, more than the chip's 32 kB. Its low-RAM mode needs 9.6 kB, or about 14.4 kB with the 2.5 kB private key and 2.4 kB signature added, and trades speed for memory by an amount nobody has published. This is an estimate from published figures, not a build.
- Why does this page give both 14 kB and 19 kB for ML-KEM-768?
- They are different code. About 14 kB is the peak stack measured for PQClean on the RP2040 (arXiv 2603.19340). About 19 kB is an estimate for mlkem-native: its published 14 kB for decapsulation plus the keys and ciphertext (2.3 kB, 1.2 kB and 1.1 kB).
- What does "No estimate" mean?
- There is no estimate or test for that board and operation.
How it was measured: kinds of evidence and how to read the tags
Measured by us: 111 results on 7 boards (CC1354P10-1, CC2755P10, MSPM0 G3519, MSPM0 G5187, MSPM33 C321A, NUCLEO-U083RC, SK-TDA4VM), in the Measured by us table above, with 47 peak-stack measurements shown beside them. The other times are TI's published figures or estimates. Researched 2026-10-09; each figure cites its source. Terms: vocabulary.
Four kinds of evidence on this page
- Demonstration, ran end to end: The exchange above, on one board, once, on 2026-10-08.
- Known-answer check, NIST vectors passed: ML-KEM-768 and ML-DSA-44 outputs match NIST's fixed inputs byte for byte, on the board, before timing.
- Measurement, cycles and stack: Times from the CPU cycle counter and stack from painting it, in the Measured by us table above.
- Assurance, none claimed: No side-channel testing, no certification (such as FIPS 140-3), no review. Nothing here says the project is secure.
A cell marked "Chip limit" means the chip cannot hold the code in its RAM or flash. Only rows marked as measured are measurements made here; none of them is a security claim.
Words in this section
Words used below, once: Cortex-M0+ is a small Arm core with no FPU (floating-point unit); Cortex-M33 is a faster Arm core; C28x is TI's control-processor core; PKA is hardware that speeds up elliptic-curve math. Encapsulation: the sender wraps a fresh secret with the receiver's public key, which gives a ciphertext (the wrapped secret); decapsulation unwraps it. kcycles means thousands of clock cycles. pqm4 is an open-source collection of post-quantum code for Cortex-M4 chips (m4fspeed is its speed-tuned assembly build); liboqs is an open-source post-quantum library; uint8_t is C's 8-bit integer type. 1 kB = 1,024 bytes.
- ML-KEM (Kyber)
- NIST's post-quantum key exchange (FIPS 203). One side publishes a public key; the other uses it to wrap a fresh shared secret (encapsulation) and sends back a ciphertext; the first side unwraps it (decapsulation). The number (512, 768, 1024) is the strength level.
- ML-DSA (Dilithium)
- NIST's post-quantum signature (FIPS 204), used to prove firmware or a message came from the right key. 44, 65 and 87 are its strength levels.
- ECDSA P-256, X25519
- Today's elliptic-curve signature and key exchange; a large enough quantum computer could break both.
- TRNG
- True random-number generator: the chip's hardware source of random bytes, which the key pair needs.
- HKDF-SHA-256
- A standard way to turn the shared secret from the exchange into an AES key of the right length.
- PKA
- Public-key accelerator: hardware on the MSPM33C321A that speeds up the elliptic-curve math.
- kcycles
- Thousands of clock cycles. Divide by the clock in MHz to get milliseconds.
- mlkem-native, mldsa-native
- Portable open-source C versions of ML-KEM and ML-DSA that run on any of these cores; TI's library covers only the MSPM0G3519 and MSPM33C321A.