Measures what it costs to authenticate a TLS client with a private key that's held in a TPM rather than in process memory.
In TLS, a client proves ownership of its certificate by signing the handshake transcript (CertificateVerify in TLS 1.3). When the private key lives in a TPM, that signature is produced inside the TPM, so the key never enters process memory and can't be copied off the machine. The trade-off is latency: a TPM signs in milliseconds where a software key signs in microseconds. This benchmark measures that per-connection cost, and how much of it you get back with TLS session resumption and connection reuse.
The TPM key is accessed through go-tpm-tls, so what gets timed is the same code path an application would use. Since that package attaches to existing keys rather than creating them, the benchmark first provisions a key the way an attestation agent would, and evicts it when it's done.
Every scenario runs twice: once with the TPM key and once with an in-memory software key of the same type and curve. The signer is wrapped in a counter, so the number of signatures per scenario is counted rather than inferred from latency.
You'll need Linux, a TPM, and root access to /dev/tpmrm0:
go build -o go-tpm-tls-bench .
sudo ./go-tpm-tls-bench
The benchmark writes a key to a persistent handle (0x81000004 by default) and
refuses to start if something already lives there. There are a few flags:
-handle: use a different persistent handle.-curve p384: measure P-384 instead of the default P-256.-evict: free a handle left behind by a previous run that was killed before
it could clean up.The tests don't need a TPM or root, and run on any platform:
go test ./...
golangci-lint run
I ran the benchmark on two Google Cloud Confidential VMs, both on Ubuntu 24.04:
an n2d-standard-2 with AMD SEV-SNP and a c3-standard-4 with Intel TDX. On
both, the vTPM is implemented in hypervisor software. Everything runs
sequentially over a single stream.
This is what the tool prints, here on the SEV-SNP machine with the default P-256 key:
TPM key at 0x81000004 on /dev/tpmrm0, curve p256, non-exportable per the TPM: true
Raw signing, 100 signatures
TPM key median 2.16 ms p95 2.50 ms 456 signature/s
software key median 0.04 ms p95 0.07 ms 21530 signature/s
TPM key, transient median 21.70 ms p95 22.19 ms 46 signature/s
Persistent key with an unrelated transient object loaded, 100 signatures
nothing loaded median 2.11 ms p95 2.48 ms 472 signature/s
same transport median 21.37 ms p95 21.88 ms 48 signature/s
other transport median 2.20 ms p95 2.52 ms 453 signature/s
One connection per request, 50 connections, no resumption
TPM key median 3.65 ms p95 4.91 ms 258 conn/s 50 signatures
software key median 1.52 ms p95 1.83 ms 635 conn/s 50 signatures
One connection per request, 50 connections, session resumption
TPM key median 1.02 ms p95 1.17 ms 906 conn/s 1 signatures 49 resumed
software key median 1.01 ms p95 1.56 ms 945 conn/s 1 signatures 49 resumed
One connection reused for 50 requests
TPM key median 0.03 ms p95 0.04 ms 31936 req/s 1 signatures
software key median 0.03 ms p95 0.04 ms 31725 req/s 1 signatures
Medians across both machines and both curves:
| SNP P-256 | SNP P-384 | TDX P-256 | TDX P-384 | |
|---|---|---|---|---|
| signature, persistent key | 2.16 ms | 2.51 ms | 1.99 ms | 3.29 ms |
| signature, transient key | 21.70 ms | 22.01 ms | 30.10 ms | 31.74 ms |
| signature, software key | 0.04 ms | 0.25 ms | 0.04 ms | 0.22 ms |
| full handshake | 3.65 ms | 4.74 ms | 3.25 ms | 5.20 ms |
| resumed handshake | 1.02 ms | 1.03 ms | 0.72 ms | 0.73 ms |
| cold handshakes per second | 258 | 204 | 299 | 189 |
Resumption and reuse don't apply to raw signing, since there's no connection involved.
A TPM signature takes single-digit milliseconds, compared to tens of microseconds in software, and it accounts for most of a full handshake: 2.16 ms out of 3.65 ms on SEV-SNP, and 1.99 ms out of 3.25 ms on TDX. Since the TPM executes one command at a time, a machine tops out at a few hundred new mutually authenticated connections per second. Note that this limit is per machine, as every machine has its own TPM.
The signature counts are identical everywhere: 50 signatures for 50 fresh connections, 1 for 50 connections with resumption, and 1 for 50 requests over a reused connection. A handshake signature can't be reused across connections, because the transcript includes fresh randomness from both peers, so each signature is only valid for a single connection. What resumption and reuse avoid is doing the handshake at all, and once the signature is out of the path, the TPM key and the software key perform within noise of each other on both machines. In other words, the cost scales with the number of handshakes, not the number of requests.
Three more things stood out, the first two of which vary by platform in size rather than direction.
Holding the key as a transient object costs about ten times as much as a persistent key on SEV-SNP, and fifteen times as much on TDX. The kernel resource manager context-saves transient objects between commands, so every signature pays for swapping the key back into the TPM, while a persistent object stays in the TPM's own storage. If you can, provision a persistent key.
That cost is charged to the transport rather than to the object. A persistent key nothing else is touching signs in 2.11 ms, and in 21.37 ms while an unrelated transient object sits loaded on the same descriptor: the same factor of ten, on a key that was never transient. It stops at the descriptor, where the same object costs 2.20 ms, so this is a property of the connection you sign on rather than of the machine. Provisioning a persistent key is necessary but not sufficient, then. The transport it signs on has to stay clear of transient objects too, and that is the part of this a workload actually controls. Measured on SEV-SNP with P-256.
A P-384 signature costs 16% more than P-256 on SEV-SNP and 65% more on TDX, which comes down to 204 and 189 cold handshakes per second respectively. Session resumption erases the difference on both platforms. This is worth knowing if a policy such as CNSA 1.0 puts you on P-384, since the extra cost only lands on cold handshakes.
Bear in mind that these numbers come from two instance types in a single cloud. The GCE vTPM is implemented in hypervisor software on both, so they say nothing about discrete TPM hardware or about a vTPM running inside the confidential VM. The two machines also differ in CPU generation as well as TEE, so treat the columns as two data points rather than a comparison of AMD against Intel. Everything runs single-threaded, since the TPM serializes commands regardless, and resumption is measured against a server that issues tickets in the same process.
main.go: the scenarios and output.provision.go: provisions the key into the TPM and evicts it afterwards.pki.go: a throwaway CA for the test certificates.bench.go: timing, signature counting, and the TLS test server.bench_test.go: asserts that the harness measures a real mTLS exchange.5 commits
Hacker News (1)
Go
100.0%
Measures what it costs to authenticate a TLS client with a private key that's held in a TPM rather than in process memory.
In TLS, a client proves ownership of its certificate by signing the handshake transcript (CertificateVerify in TLS 1.3). When the private key lives in a TPM, that signature is produced inside the TPM, so the key never enters process memory and can't be copied off the machine. The trade-off is latency: a TPM signs in milliseconds where a software key signs in microseconds. This benchmark measures that per-connection cost, and how much of it you get back with TLS session resumption and connection reuse.
The TPM key is accessed through go-tpm-tls, so what gets timed is the same code path an application would use. Since that package attaches to existing keys rather than creating them, the benchmark first provisions a key the way an attestation agent would, and evicts it when it's done.
Every scenario runs twice: once with the TPM key and once with an in-memory software key of the same type and curve. The signer is wrapped in a counter, so the number of signatures per scenario is counted rather than inferred from latency.
You'll need Linux, a TPM, and root access to /dev/tpmrm0:
go build -o go-tpm-tls-bench .
sudo ./go-tpm-tls-bench
The benchmark writes a key to a persistent handle (0x81000004 by default) and
refuses to start if something already lives there. There are a few flags:
-handle: use a different persistent handle.-curve p384: measure P-384 instead of the default P-256.-evict: free a handle left behind by a previous run that was killed before
it could clean up.The tests don't need a TPM or root, and run on any platform:
go test ./...
golangci-lint run
I ran the benchmark on two Google Cloud Confidential VMs, both on Ubuntu 24.04:
an n2d-standard-2 with AMD SEV-SNP and a c3-standard-4 with Intel TDX. On
both, the vTPM is implemented in hypervisor software. Everything runs
sequentially over a single stream.
This is what the tool prints, here on the SEV-SNP machine with the default P-256 key:
TPM key at 0x81000004 on /dev/tpmrm0, curve p256, non-exportable per the TPM: true
Raw signing, 100 signatures
TPM key median 2.16 ms p95 2.50 ms 456 signature/s
software key median 0.04 ms p95 0.07 ms 21530 signature/s
TPM key, transient median 21.70 ms p95 22.19 ms 46 signature/s
Persistent key with an unrelated transient object loaded, 100 signatures
nothing loaded median 2.11 ms p95 2.48 ms 472 signature/s
same transport median 21.37 ms p95 21.88 ms 48 signature/s
other transport median 2.20 ms p95 2.52 ms 453 signature/s
One connection per request, 50 connections, no resumption
TPM key median 3.65 ms p95 4.91 ms 258 conn/s 50 signatures
software key median 1.52 ms p95 1.83 ms 635 conn/s 50 signatures
One connection per request, 50 connections, session resumption
TPM key median 1.02 ms p95 1.17 ms 906 conn/s 1 signatures 49 resumed
software key median 1.01 ms p95 1.56 ms 945 conn/s 1 signatures 49 resumed
One connection reused for 50 requests
TPM key median 0.03 ms p95 0.04 ms 31936 req/s 1 signatures
software key median 0.03 ms p95 0.04 ms 31725 req/s 1 signatures
Medians across both machines and both curves:
| SNP P-256 | SNP P-384 | TDX P-256 | TDX P-384 | |
|---|---|---|---|---|
| signature, persistent key | 2.16 ms | 2.51 ms | 1.99 ms | 3.29 ms |
| signature, transient key | 21.70 ms | 22.01 ms | 30.10 ms | 31.74 ms |
| signature, software key | 0.04 ms | 0.25 ms | 0.04 ms | 0.22 ms |
| full handshake | 3.65 ms | 4.74 ms | 3.25 ms | 5.20 ms |
| resumed handshake | 1.02 ms | 1.03 ms | 0.72 ms | 0.73 ms |
| cold handshakes per second | 258 | 204 | 299 | 189 |
Resumption and reuse don't apply to raw signing, since there's no connection involved.
A TPM signature takes single-digit milliseconds, compared to tens of microseconds in software, and it accounts for most of a full handshake: 2.16 ms out of 3.65 ms on SEV-SNP, and 1.99 ms out of 3.25 ms on TDX. Since the TPM executes one command at a time, a machine tops out at a few hundred new mutually authenticated connections per second. Note that this limit is per machine, as every machine has its own TPM.
The signature counts are identical everywhere: 50 signatures for 50 fresh connections, 1 for 50 connections with resumption, and 1 for 50 requests over a reused connection. A handshake signature can't be reused across connections, because the transcript includes fresh randomness from both peers, so each signature is only valid for a single connection. What resumption and reuse avoid is doing the handshake at all, and once the signature is out of the path, the TPM key and the software key perform within noise of each other on both machines. In other words, the cost scales with the number of handshakes, not the number of requests.
Three more things stood out, the first two of which vary by platform in size rather than direction.
Holding the key as a transient object costs about ten times as much as a persistent key on SEV-SNP, and fifteen times as much on TDX. The kernel resource manager context-saves transient objects between commands, so every signature pays for swapping the key back into the TPM, while a persistent object stays in the TPM's own storage. If you can, provision a persistent key.
That cost is charged to the transport rather than to the object. A persistent key nothing else is touching signs in 2.11 ms, and in 21.37 ms while an unrelated transient object sits loaded on the same descriptor: the same factor of ten, on a key that was never transient. It stops at the descriptor, where the same object costs 2.20 ms, so this is a property of the connection you sign on rather than of the machine. Provisioning a persistent key is necessary but not sufficient, then. The transport it signs on has to stay clear of transient objects too, and that is the part of this a workload actually controls. Measured on SEV-SNP with P-256.
A P-384 signature costs 16% more than P-256 on SEV-SNP and 65% more on TDX, which comes down to 204 and 189 cold handshakes per second respectively. Session resumption erases the difference on both platforms. This is worth knowing if a policy such as CNSA 1.0 puts you on P-384, since the extra cost only lands on cold handshakes.
Bear in mind that these numbers come from two instance types in a single cloud. The GCE vTPM is implemented in hypervisor software on both, so they say nothing about discrete TPM hardware or about a vTPM running inside the confidential VM. The two machines also differ in CPU generation as well as TEE, so treat the columns as two data points rather than a comparison of AMD against Intel. Everything runs single-threaded, since the TPM serializes commands regardless, and resumption is measured against a server that issues tickets in the same process.
main.go: the scenarios and output.provision.go: provisions the key into the TPM and evicts it afterwards.pki.go: a throwaway CA for the test certificates.bench.go: timing, signature counting, and the TLS test server.bench_test.go: asserts that the harness measures a real mTLS exchange.Hacker News (1)
5 commits
Go
100.0%