wolfTPM/examples/pqc
Aidan Garske a89169ef05 Include SPDM, PQC, test and CMake files in the source distribution 2026-09-14 13:28:18 -07:00
..
README.md Add SealSQ QVault TPM post-quantum support and pqc_ctrl example 2026-08-14 19:44:00 +01:00
gen_pqc_certs.c Fold PQC TLS into tls_client, tls_server and run_examples.sh 2026-07-28 15:27:48 -07:00
include.am Include SPDM, PQC, test and CMake files in the source distribution 2026-09-14 13:28:18 -07:00
mldsa_sign.c F-6322 - Correct ML-DSA sign sequence comments to streamable 2026-06-22 12:03:50 -07:00
mldsa_verify_neg.c F-6322 - Correct ML-DSA sign sequence comments to streamable 2026-06-22 12:03:50 -07:00
mlkem_decap_neg.c Add fine-grained PQC build macros to trim v1.85 ML-DSA/ML-KEM by algorithm and operation 2026-06-15 15:49:34 -07:00
mlkem_encap.c Add fine-grained PQC build macros to trim v1.85 ML-DSA/ML-KEM by algorithm and operation 2026-06-15 15:49:34 -07:00
pqc_ctrl.c Add SealSQ QVault TPM post-quantum support and pqc_ctrl example 2026-08-14 19:44:00 +01:00
pqc_ctrl.sh Add SealSQ QVault TPM post-quantum support and pqc_ctrl example 2026-08-14 19:44:00 +01:00
pqc_mssim_e2e.c Support ML-KEM credential activation and ML-DSA quotes in fwTPM 2026-09-07 18:44:02 -07:00

README.md

wolfTPM TPM 2.0 v1.85 Post-Quantum Examples

Examples exercising the ML-DSA / ML-KEM post-quantum additions from TCG TPM 2.0 Library Specification v1.85, wrapped by wolfTPM2_* API calls.

These examples run on the SealSQ QVault TPM — the first shipping TPM 2.0 with v1.85 post-quantum (ML-DSA / ML-KEM) algorithms in silicon — over SPI, and on the in-tree fwTPM server for CI or when no hardware is present. Build for the SealSQ part with --enable-sealsq --enable-pqc; see docs/FWTPM.md for the fwTPM PQC reference.

Building

wolfSSL (ML-DSA and ML-KEM in wolfCrypt):

./configure --enable-wolftpm --enable-mldsa --enable-mlkem \
            --enable-tls-mlkem-standalone --enable-experimental \
            --enable-harden --enable-keygen --enable-certgen
make
sudo make install

--enable-tls-mlkem-standalone is required for the standalone ML_KEM_* TLS groups; without it wolfSSL only offers the hybrid groups and wolfSSL_UseKeyShare rejects the client default.

--enable-certgen is needed by the TLS gen_pqc_certs tool below; --enable-wolftpm provides the crypto callback and private-key-id support the TLS server uses.

wolfTPM:

./configure --enable-fwtpm --enable-pqc
make

--enable-pqc is an alias for --enable-v185. If you omit both but --enable-fwtpm is set and wolfCrypt has ML-DSA + ML-KEM, configure auto-enables PQC.

Run the test suite

make check

Runs the full suite, including all PQC coverage:

  • tests/fwtpm_unit.test — 30+ in-process PQC handler tests
  • tests/unit.test — PQC wrapper tests over the mssim socket (ML-DSA Sign/Verify Sequence, ML-KEM Encap/Decap, EncryptSecret MLKEM, etc.)
  • tests/pqc_mssim_e2e.sh — dedicated PQC end-to-end round-trip

The individual scripts make check invokes are also runnable directly for faster targeted iteration:

./tests/fwtpm_check.sh        # fwtpm_unit.test + unit.test + tpm2_tools suite
./tests/pqc_mssim_e2e.sh      # PQC E2E only (fastest PQC-focused check)

Individual examples

All examples expect a running fwtpm_server on 127.0.0.1:2321:

./src/fwtpm/fwtpm_server --clear &

pqc_ctrl — PQC control center

One CLI to drive and validate a PQC TPM (SealSQ QVault TPM, or the fwTPM), modeled on examples/spdm/spdm_ctrl: each command runs an operation and controls the board. Every key operation flushes the transient object table first, so a TPM with a small object memory (e.g. SealSQ QVault TPM) does not hit TPM_RC_OBJECT_MEMORY when commands are chained.

./examples/pqc/pqc_ctrl                 # --all (default)
./examples/pqc/pqc_ctrl --caps --algs   # identify + list supported algorithms
./examples/pqc/pqc_ctrl --mldsa=87      # ML-DSA-87 sign/verify
./examples/pqc/pqc_ctrl --mlkem=1024    # ML-KEM-1024 encap/decap
./examples/pqc/pqc_ctrl --selftest --getrandom=32 --pcrread=0
Command Description
--caps Manufacturer, vendor string, firmware, FIPS mode
--algs List the algorithms the TPM reports as supported
--selftest TPM2_SelfTest
--getrandom[=N] N random bytes (default 16)
--pcrread[=idx] Read a PCR (SHA-256 bank, falling back to SHA-384)
--pcrextend=idx Extend a PCR with a test digest (explicit index required)
--flush Flush transient objects (board reset between ops)
--clear TPM2_Clear — wipes the owner hierarchy
--mldsa[=44/65/87] Pure ML-DSA sign/verify (default 65)
--hash-mldsa[=44/65/87] Hash-ML-DSA (SHA-256 pre-hash) sign/verify
--mlkem[=512/768/1024] ML-KEM encapsulate/decapsulate
--all caps + algs + selftest + getrandom + pcrread + every PQC set

Commands run left-to-right, so they can be chained. Requires --enable-v185 (or --enable-pqc). Point it at the SealSQ part with --enable-sealsq, or at the fwTPM with --enable-fwtpm --enable-swtpm.

Run the whole command set as a pass/fail suite (mirrors examples/spdm/spdm_test.sh). The destructive --clear is opt-in via PQC_CTRL_CLEAR=1 so the suite never wipes a TPM unexpectedly:

./examples/pqc/pqc_ctrl.sh
PQC_CTRL_CLEAR=1 ./examples/pqc/pqc_ctrl.sh   # also exercise TPM2_Clear

Benchmarks on SealSQ QVault TPM silicon

Measured with examples/bench/bench on a Raspberry Pi 5 driving the QVault TPM over SPI (the first post-quantum TPM benchmarks on shipping-class silicon):

Operation Avg latency Throughput
ML-DSA-65 key gen 2044.7 ms 0.49 ops/s
ML-DSA-65 sign 581.0 ms 1.72 ops/s
ML-DSA-65 verify 163.1 ms 6.13 ops/s
ML-KEM-768 key gen 800.8 ms 1.25 ops/s
ML-KEM-768 encapsulate 211.8 ms 4.72 ops/s
ML-KEM-768 decapsulate 425.5 ms 2.35 ops/s

Verification is fast (comparable to ECDSA); key generation is a one-off provisioning cost. See the top-level README.md TPM2 Benchmarks section for the full classical + PQC run.

pqc_mssim_e2e

End-to-end client test over the mssim socket. Two round-trips:

  1. MLKEM-768 CreatePrimary + Encapsulate + Decapsulate. Asserts ciphertext is 1088 bytes and the two shared secrets are byte-identical.
  2. HashMLDSA-65 (SHA-256) CreatePrimary + SignDigest + VerifyDigestSignature. Asserts the signature is 3309 bytes and the validation ticket tag is TPM_ST_DIGEST_VERIFIED.
./examples/pqc/pqc_mssim_e2e

mlkem_encap

ML-KEM encapsulation round-trip. Creates a primary ML-KEM key, runs Encapsulate, then Decapsulates the produced ciphertext and confirms the shared secrets match.

./examples/pqc/mlkem_encap                # default: MLKEM-768
./examples/pqc/mlkem_encap -mlkem=512
./examples/pqc/mlkem_encap -mlkem=1024

mldsa_sign

Pure ML-DSA sign+verify round-trip. Creates a primary ML-DSA key, signs a fixed message via SignSequenceStart + SignSequenceComplete (Pure ML-DSA is one-shot per Part 3 Sec.17.5, so the message rides on the Complete buffer), then verifies via VerifySequenceStart + VerifySequenceUpdate + VerifySequenceComplete (Sec.20.3 allows Update on verify sequences). Asserts the returned validation ticket tag is TPM_ST_MESSAGE_VERIFIED.

./examples/pqc/mldsa_sign                 # default: MLDSA-65
./examples/pqc/mldsa_sign -mldsa=44
./examples/pqc/mldsa_sign -mldsa=87

PQC keys via keygen / keyload

examples/keygen/keygen accepts v1.85 PQC options alongside -rsa, -ecc, -sym, and -keyedhash:

./examples/keygen/keygen keyblob.bin -mldsa=65           # Pure ML-DSA
./examples/keygen/keygen keyblob.bin -hash_mldsa=65      # SHA-256 pre-hash
./examples/keygen/keygen keyblob.bin -mlkem=768          # ML-KEM

Parameter sets:

  • -mldsa=44|65|87 (default 65)
  • -hash_mldsa=44|65|87 (default 65, SHA-256 pre-hash)
  • -mlkem=512|768|1024 (default 768)

Verify the produced blob round-trips through TPM2_Create + TPM2_Load by loading it back:

./examples/keygen/keyload keyblob.bin

A successful load prints Loaded key to 0x80000000. The full 18-way matrix (three variants x three parameter sets) is exercised by examples/run_examples.sh when v1.85 is detected in config.h.

PQC keys for parameter encryption

A post-quantum primary can key a TPM 2.0 parameter-encryption session: ML-KEM (decrypt capable) is used as the session salt key and ML-DSA (sign only) as the session bind key. The session protects the command's first sized parameter the same way an RSA/ECC salted session does. Any RSA/ECC storage key the example needs (for example the parent of a created child) is unchanged.

Note on ML-DSA confidentiality: parameter-encryption confidentiality comes from the session key, which a bound session derives from the bind entity's authValue (TPM 2.0 Library Part 1, Salted Session). A sign-only ML-DSA key cannot exchange a salt, and the example's bind authValue is a public constant, so an ML-DSA bind alone provides session binding but no confidentiality against a bus observer. To keep the advertised encryption real, the helper additionally creates a transient SRK and uses it as the asymmetric salt for the ML-DSA session: confidentiality comes from the encrypted salt while the ML-DSA key supplies the binding. A real deployment that relies on a bare bound session for confidentiality must use a bind entity whose authValue is secret and was not sent in cleartext.

wrap_test, pcr/quote, and nvram/store and nvram/counter take -mlkem[=512|768|1024] and -mldsa[=44|65|87]. keygen uses -paramkey=mlkem[=...] / -paramkey=mldsa[=...] because its -mlkem / -mldsa already select the child key algorithm.

./examples/wrap/wrap_test -aes -mlkem=768
./examples/pcr/quote 16 quote.blob -ecc -xor -mldsa=65
./examples/nvram/counter -aes -mldsa=65
./examples/keygen/keygen keyblob.bin -ecc -aes -paramkey=mlkem=768

ML-KEM is a restricted decryption (salt) key, which requires a symmetric definition; the example helper sets AES-128-CFB on it (a TPM rejects a restricted key with no symmetric algorithm via TPM_RC_SYMMETRIC).

create_primary ML-DSA primary

examples/keygen/create_primary can create an ML-DSA primary key:

./examples/keygen/create_primary -mldsa            # default MLDSA-65
./examples/keygen/create_primary -mldsa=87 -oh

Post-Quantum TLS 1.3 (ML-KEM + TPM ML-DSA)

A full TLS 1.3 handshake where the server's ML-DSA identity key lives in the TPM. The server signs the CertificateVerify on-chip via the wolfTPM crypto callback; the client performs an ML-KEM key exchange and validates the server against a software CA.

Requires a wolfSSL that routes wc_MlDsaKey_SignCtx to the crypto callback for device keys (private key in the TPM). That landed upstream, so master or any later release works. No shipping TPM implements TCG v1.85 PQC yet, so this runs against the in-tree fwTPM.

Demo scope: the identity key is an unauthenticated deterministic TPM primary (empty auth), reproducible by both gen_pqc_certs and the server from the owner hierarchy. A production deployment should protect the identity key with a non-empty auth value or policy so it cannot be recreated from the public cert. The client validates the server chain against the demo CA but does not bind the certificate to the host name, so the demo connects to the default localhost and does not pass -h=. Supplying -h= turns on strict verification including wolfSSL_check_domain_name, which this leaf cannot satisfy; a production deployment should issue the leaf with a matching subjectAltName.

Three programs:

  • examples/pqc/gen_pqc_certs — makes a software ML-DSA CA and a device leaf cert whose subject key is the TPM ML-DSA key.
  • examples/tls/tls_server -mldsa — recreates that TPM key and serves TLS 1.3.
  • examples/tls/tls_client -mldsa — connects, ML-KEM key exchange, verifies the CA.
./src/fwtpm/fwtpm_server --clear &

# 1. certificate chain bound to the TPM key (-mldsa must match the server)
./examples/pqc/gen_pqc_certs -mldsa=65

# 2. server (same -mldsa as gen_pqc_certs)
./examples/tls/tls_server -p=11111 -mldsa=65 &

# 3. client (choose the ML-KEM group)
./examples/tls/tls_client -p=11111 -mldsa -group=ML_KEM_768

Options:

  • gen_pqc_certs -mldsa=44/65/87 — ML-DSA parameter set.
  • tls_server -p=<port> -mldsa=44/65/87.
  • tls_client -h=<host> -p=<port> -group=<name> where <name> is ML_KEM_512/768/1024 or a hybrid SECP256R1MLKEM768 / X25519MLKEM768 (hybrids need the matching classical curve enabled in wolfSSL).

The one-shot end-to-end test drives all three and asserts the ML-KEM group, TPM-signed ML-DSA authentication, CA verification, and app data:

ENABLE_PQC_TLS=1 ./examples/run_examples.sh   # includes the PQC TLS matrix