🇯🇵 この記事の日本語版 → TPMに、Rustだけでポスト量子を喋らせた
TL;DR
- What this is: the first Rust-native implementation of the TPM 2.0 v1.85 post-quantum commands (ML-KEM / ML-DSA) — on crates.io as
hyde-tpm 0.2.0, with no libtss2 and no C. - What it is NOT: not the first PQC in a TPM — wolfTPM did that first, in C. And not on real hardware: no shipping silicon implements v1.85 PQC yet, so it runs against a firmware TPM (a real Infineon SLB9670 rejects the commands — see the last section).
- Evidence: command marshalling is checked byte-for-byte against the C implementation, and a TPM-produced ML-DSA signature is independently verified by RustCrypto’s
ml-dsa(FIPS 204 VALID). Both run in CI with no TPM. - Status: experimental
0.2.xproof-of-concept — some parameters provisional, response decoding hand-written. Use at your own risk.
TPM 2.0 spec v1.85 finally brings post-quantum (ML-KEM / ML-DSA) into the TPM’s standard command set. Let me be honest up front: I was not the first to run PQC in a TPM. wolfTPM (in C) got there first — PR #445, this March. What I did is a different thing: run v1.85 PQC Rust-natively, with zero dependency on libtss2 or any C, in a form that ships on crates.io. That lane was empty. I planted the first marker in it — and I did it while using the C implementation as an oracle, checking that my own bytes matched it to the byte.
Why the Rust lane was empty — tss-esapi and the non_exhaustive wall
Rust’s go-to crate tss-esapi is a thin wrapper over C’s libtss2, and libtss2 has no v1.85 PQC yet — so that path simply can’t speak it. The pure-Rust, no_std tpm2-protocol (Jarkko Sakkinen) is beautifully designed, but its command and algorithm-ID enums are #[non_exhaustive]: downstream code cannot add new commands — touching them requires a fork. Both doors were shut. That is why the lane was empty.
Using C as an oracle — checking to the byte
Read the spec, marshal the v1.85 commands in pure Rust. But “probably spec-conformant” isn’t trust. Precision is trust. So I made the already-working C implementation the oracle: capture the exact bytes wolfTPM puts on the wire, and diff them against the bytes my Rust emits.
// for encapsulate / decapsulate / sign / verify …
assert_eq!(rust_marshalled, c_reference_wire); // A == B, not one byte off
The command marshalling is checked byte-for-byte against reference wire from an independent C client. This check runs without a TPM (it’s baked into the tests) — reproducible in CI, no hardware required.
Grafting it on, and actually talking — round-trips against a firmware TPM
I forked tpm2-protocol and grafted the v1.85 layer onto it (command codes 0x1A3–0x1AA; algorithm IDs ML-KEM=0x00A0 / ML-DSA=0x00A1), then ran round-trips against a firmware TPM over the mssim socket (127.0.0.1:2321).
- ML-KEM: the shared secret encapsulated on the host matched the one the TPM decapsulated (encap == decap).
- ML-DSA: the TPM generated a signing key and signed a message, returning a 1312-byte verifying key and a 2420-byte signature (ML-DSA-44). The TPM’s own Verify command then returned a validation ticket.
So not just “it marshals” — the TPM accepts it and returns a signature. The public API is this thin:
use hyde_tpm::pqc::{MlDsa, MlKem, PqcTpm};
let mut tpm = PqcTpm::connect("127.0.0.1:2321")?;
let secret = tpm.ml_kem_roundtrip(MlKem::K512)?; // KEM: encap == decap
let (pk, sig) = tpm.ml_dsa_sign(MlDsa::D44, message)?; // hardware-rooted signature
Checking again with an independent implementation — FIPS 204 VALID
I verified the TPM-produced signature with a completely separate implementation (RustCrypto’s ml-dsa). Result: VALID — a third-party implementation confirms the signature is correct under FIPS 204.
ml_dsa::verify_with_context(msg, &[], sig) // ctx = empty (pure ML-DSA) => VALID
But I draw the line honestly: this is evidence, not attestation. Verifying a TPM signature on the host does not replace TPM-rooted attestation verification. So the independent verifier is confined to tests and examples, never the public API. Evidence travels with the crate — but evidence is treated as evidence.
Shipping it — hyde-tpm 0.2.0 on crates.io
Published as hyde-tpm 0.2.0, split by feature gate: the default tss path (classic tss-esapi) is left completely unchanged, and a pqc feature carries the pure-Rust v1.85 path — buildable with no libtss2, no C, std only.
# PQC path, builds & tests without libtss2
cargo test -p hyde-tpm --no-default-features --features pqc
Rust-native v1.85 PQC is now something anyone can pull with cargo — the first published Rust implementation. The priority marker is planted.
And real silicon still says no — which is the proof of the frontier
I sent the same v1.85 commands to a real chip — an Infineon SLB9670 (/dev/tpmrm0). It answered TPM_RC_COMMAND_CODE: “I don’t know that command.” Yet a classic GetRandom to the same chip succeeds — the chip is alive; there just isn’t any silicon yet that implements v1.85 PQC. That refusal is the proof of where we stand: right past the leading edge where an implementation exists (the firmware TPM), on the edge silicon hasn’t reached. Not a weakness — the frontier.

TPM_RC_COMMAND_CODE while accepting classic commands. The frontier, in the flesh.Why the urgency? CNSA 2.0 puts firmware signing at the earliest deadline (exclusive use by 2030, with a procurement gate in 2027). PQC in the TPM feeds directly into that. Paving the Rust road before the silicon arrives is the point.
Limitations, not diluted
- Firmware TPM only. No hardware TPM implements v1.85 PQC yet, so this is demonstrated against the only existing implementation — a firmware TPM. Real-silicon validation awaits shipping hardware. The transport is TCTI-shaped, so the port is expected to be unchanged, but this is unverified.
- Some parameters are provisional. A few Sequence-Start fields are pinned to observed wire. They are functionally successful (the TPM accepts them and Verify returns a ticket); what’s outstanding is cross-referencing their byte layout against the TCG Part 3 spec — documentation, not whether they work.
- Experimental (0.2.x), proof-of-concept. Response decoding is hand-written; the v1.85 additions are maintained as a fork-derived layer. Use at your own risk.
A different coordinate
C got there first. That’s a fact, and I won’t hide it. What I planted is a different coordinate — Rust only, no C, in a form that ships on crates.io, checked byte-for-byte against C as the oracle. With the same certainty I felt writing “byte-identical TS came out, not one byte off” in the one-seg series, I got a TPM to speak post-quantum. Next, it’s silicon’s turn.
- crates.io: hyde-tpm
- Source: gitlab.com/Ryujiyasu/hyde
- 🇯🇵 日本語版 → TPMに、Rustだけでポスト量子を喋らせた

コメントを残す