go-tpm-tls signs TLS handshakes inside a TPM, not a key file
Bruno Schaatsbergen built go-tpm-tls, a Go library that signs the TLS handshake's CertificateVerify message with a private key that lives inside a machine's Trusted Platform Module (TPM) instead of a key file on disk. His reasoning is concrete: copy a private-key file and you are that machine everywhere it is trusted, the secret store, the internal API that only accepts client certificates, the database that maps a certificate subject to a role. A key held in the TPM cannot be extracted that way; it can only be used, and only from that machine.
He benchmarked the performance cost on Google Cloud Confidential VMs using P-256 keys. On an n2d-standard-2 instance with AMD SEV-SNP, raw signing with a persistent TPM key took a median 2.21 ms (452 signatures per second over 100 runs), against 0.06 ms (16,591 signatures per second) for a software key. For a full new TLS connection with no session resumption, a TPM key took a median 3.35 ms (295 connections per second, 50-connection run) versus 1.38 ms (707 connections per second) for a software key. That gap nearly closes once TLS session resumption is used: over a 50-connection run with one fresh handshake and 49 resumed connections, the TPM key came out at 0.94 ms per connection against 0.95 ms for the software key. Reusing a single connection for 50 requests, which needs only one signature total, brought the TPM-backed cost down to 0.02 ms per request (40,043 requests per second).
Several findings shaped the numbers. The signature itself is most of a cold handshake: 2.21 ms of the 3.35 ms total. A transient TPM key handle, one created and evicted rather than kept resident, costs about ten times as much per signature as a persistent key on the SEV-SNP machine, and about fifteen times as much on Intel TDX. TPM operations also serialize per file descriptor rather than per key: a persistent key alone on its own descriptor signs in 2.08 ms, the same key slows to 20.04 ms when an unrelated transient object shares that descriptor, and moving it back to a dedicated descriptor returns it to 2.10 ms. Comparing elliptic curves on the SEV-SNP machine, a P-384 key signed in 2.37 ms against P-256's 2.21 ms (226 versus 295 cold handshakes per second); an earlier run on the same instance type had put that P-384 overhead at 16% rather than the 7% implied by the run reported in the table, a gap the author calls small and noisy rather than resolved, while the same P-384-versus-P-256 comparison on Intel TDX came out at 65%.
The design has hard limits. Because a TPM can only run one signing operation at a time, serialized behind a lock with no timeout, a single machine tops out at a few hundred new mutually authenticated connections per second, a ceiling the author notes also works as a usable denial-of-service vector against a public listener holding a TPM key. go-tpm-tls deliberately has no code to create or evict a TPM key; it only attaches to a key that a separate provisioning process, typically an attestation agent, has already created, and only an unrestricted signing key works: a restricted key such as an attestation key will refuse to sign a caller-supplied TLS transcript hash, because TPM2_Sign requires a validation ticket that only the TPM's own hashing operation can produce. He also flags a TLS 1.3 quirk: the client sends its Certificate, CertificateVerify and Finished in one flight without waiting to learn whether the server accepted them, so a health check that only dials a connection without reading a byte back will report success even when the server has rejected the client's certificate.
The author is explicit about what this does not solve. A TPM-signed handshake proves the connection comes from the machine holding the key and that it is happening now rather than being replayed, but it says nothing about whether the code running on that machine is trustworthy, and it does not protect the session's traffic once established. He does not claim a TPM is more secure than a well-run KMS; its advantage is that it needs no separate credential to reach and is already present on most machines built in the last decade. It also does not defend against an attacker who already has persistent code execution and is using the machine's identity while still on the box, a different problem, he says, that a TPM does not solve. He says he is now looking at SPIFFE integration and will write that up next.
Key facts
- go-tpm-tls is a Go library that signs TLS handshakes with a private key held inside a machine's TPM instead of a key file on disk, so the key cannot be copied off the machine.
- Raw signing with a persistent TPM key: 2.21 ms median, 452 signatures per second, versus 0.06 ms and 16,591 signatures per second for a software key (P-256, Google Cloud n2d-standard-2, AMD SEV-SNP, 100-run benchmark).
- TLS session resumption nearly erases the gap: 0.94 ms per connection with a TPM key versus 0.95 ms with a software key; reusing one connection for 50 requests drops the TPM-backed cost to 0.02 ms per request.
- A transient (non-resident) TPM key costs about ten times more per signature than a persistent key on the SEV-SNP machine tested, and about fifteen times more on Intel TDX; TPM signing serializes per file descriptor, capping a machine at a few hundred new authenticated connections per second.
- The author stresses the limits: a TPM proves the connection is from that machine right now, not that its code is trustworthy, and he does not claim it is more secure than a well-run KMS.
Why it matters
A private TLS client key stored as a file on disk is a single point of failure: read-only access to a machine's memory or storage lets an attacker copy the key and, in the author's words, act as that machine everywhere it is trusted, the secret store, an internal API gated on client certificates, a database that maps a certificate subject to a role. go-tpm-tls closes that specific gap by keeping the signing operation inside the TPM chip already present on most machines built in the last decade, so the key material never has to sit in a file or in process memory that an attacker could read. The fact that TLS session resumption erases almost all of the measured performance cost makes this a workable hardening step for machine-to-machine mTLS rather than a theoretical exercise.
Who it affects
It is aimed at engineers building Go services that authenticate to each other over mutual TLS: internal APIs that only accept client certificates, secret stores, and systems that map a certificate's subject to an access role. It assumes a machine-identity setup where a separate provisioning process, typically an attestation agent, has already created a TPM key; go-tpm-tls only attaches to that key and deliberately provides no way to create or evict one itself.
How to use it
go-tpm-tls is a Go library that attaches to an existing persistent TPM key, identified by a handle in the range 0x81000000 to 0x81FFFFFF (0x81000004 in the author's example), rather than creating one. It requires an unrestricted signing key: a restricted key such as an attestation key will refuse to sign a caller-supplied TLS transcript hash, since TPM2_Sign requires a validation ticket that only the TPM's own hashing operation produces. The author says he is now looking at SPIFFE integration and will write that up next; the source gives no pricing or licence terms.
How solid is it
The numbers come from the author's own benchmarks on Google Cloud Confidential VMs: an n2d-standard-2 instance with AMD SEV-SNP for the main P-256 comparisons, and a c3-standard-4 with Intel TDX for the transient-key and P-384 checks. Each figure carries a run count alongside median and higher-percentile timings, for example the persistent-key raw-signing benchmark covers 100 signatures at a 2.21 ms median. The author flags one inconsistency himself: an earlier run on the same SEV-SNP instance type put the P-384-versus-P-256 signing overhead at 16%, against the 7% implied by the run he reports in the table, and he calls that gap small and noisy rather than resolving it.
Risks and caveats
By the author's own account, a TPM-signed handshake proves only that the connection came from the machine holding the key and that it is happening now; it says nothing about whether the code running on that machine is trustworthy, does not protect the session's traffic once established, and does not defend against an attacker who already has persistent code execution on the box. Because a TPM can run only one signing operation at a time, serialized behind a lock with no timeout, a machine tops out at a few hundred new mutually authenticated connections per second, a ceiling the author notes also functions as a usable denial-of-service vector against a public listener holding a TPM key. He also warns that TLS 1.3's single-flight handshake means a health check that only dials a connection without reading a response back will report success even when the server has rejected the client's certificate. The benchmark numbers are scoped to hypervisor-implemented vTPMs on the two Google Cloud machine types tested and, by the author's own statement, say nothing about discrete TPM hardware.
“Copy it and you are that machine everywhere that machine is trusted: the secret store, the internal API that only accepts client certificates, the database that maps a certificate subject to a role.”
— Bruno Schaatsbergen, author of go-tpm-tls