TPM2 IDENTITY
TPM2-backed mTLS identity (v0.1.4, optional)
identity.backend = "tpm" moves the mTLS private key introduced in
docs/MTLS_ENROLLMENT.md into a TPM2, so it is generated and signs entirely
inside the chip instead of living as a PKCS#8 file on disk. Fully optional
and feature-gated (--features tpm2, off by default) - it must never affect
a plain cargo build, and most dev/test machines have no TPM at all.
What actually changes¶
Everything else about enrollment and mTLS serving stays the same (same
enroll/identity commands, same wire protocol, same auth.mode = "mtls"
listener). Only where the private key lives and signs changes:
- software (default):
rcgen/ringgenerate an ECDSA P-256 key; it's written toauth.mtls.key_fileas a plain PKCS#8 PEM file. - tpm: a
tss-esapiTransientKeyContextasks the TPM to generate an ECDSA P-256 key. What's written toauth.mtls.key_fileinstead is a small JSON envelope holding the TPM's public key plus an encrypted private key blob that only the same TPM can unwrap - never a raw PKCS#8 key. Every signature (the CSR at enrollment time, every TLS handshake while serving) re-opens a TPM session and asks the chip to sign; the private key's sensitive area is decrypted by the TPM itself and never exists in usable form in this process's memory.
zyvor-device-agent identity reports which backend produced the current
key (backend: software or backend: tpm).
Runtime fallback and policy¶
identity.policy decides whether a TPM failure is fatal:
| Policy | Behavior |
|---|---|
software |
Always a file key |
preferred (default) |
Try TPM when backend = "tpm", else fall back |
required |
Fail enroll and serve if the TPM cannot open |
Production Minew snippets use policy = "required". CI and laptops keep
preferred.
Configuration¶
[identity]
backend = "tpm"
tpm_tcti = "device:/dev/tpmrm0" # or e.g. "swtpm:host=127.0.0.1,port=2321"
tpm_tcti is a tss-esapi TCTI connection string. Empty uses
tss-esapi's own environment-variable-based default resolution
(TPM2TOOLS_TCTI/TCTI). A real TPM is typically device:/dev/tpmrm0
(the kernel resource-managed TPM device node, preferred over /dev/tpm0
so multiple processes can share the chip safely).
Why ECC P-256 only¶
The tpm2 feature always uses ECDSA P-256/SHA-256 - the combination every
TPM2 chip and swtpm support well - rather than exposing a wider algorithm
choice. This keeps the DER signature encoding and public-key handling in
src/identity/tpm.rs simple and narrows what needs security review.
Live-verified with swtpm¶
This was verified end to end against swtpm (the TPM2 emulator used for
development and CI where no physical TPM is available), not just
compile-checked:
swtpm socket --tpm2 --tpmstate dir=/var/lib/swtpm --ctrl type=tcp,port=2322 \
--server type=tcp,port=2321 &
swtpm_ioctl --tcp :2322 -i # one-time hardware init
tpm2_startup --clear --tcti=swtpm:host=127.0.0.1,port=2321
# identity.backend = "tpm", identity.tpm_tcti = "swtpm:host=127.0.0.1,port=2321"
zyvor-device-agent --config device-agent.toml enroll
zyvor-device-agent --config device-agent.toml identity # backend: tpm
zyvor-device-agent --config device-agent.toml serve # auth.mode = "mtls"
Confirmed: the persisted auth.mtls.key_file is the JSON envelope described
above (not a PKCS#8 key), identity reports backend: tpm, and a real mTLS
handshake against the running serve process succeeds - i.e. the TLS
signature the TPM produced during the handshake verifies correctly against
the certificate's public key.
Non-goals¶
No support for moving an existing software key into a TPM, or vice versa -
enroll --force against the desired backend issues a fresh key either way.
Multiple simultaneous identities (e.g. software while migrating to TPM) are
not supported; auth.mtls.key_file holds exactly one.