GitHub - bschaatsbergen/go-tpm-tls: A, Go, crypto.Signer backed by a TPM key, so crypto/tls can authenticate with a key that never leaves the hardware.

3 min read Original article ↗

Go Reference

Provides a crypto.Signer backed by a key in a TPM, so crypto/tls can authenticate a client or server with a key that never leaves the TPM. It is a small layer over go-tpm and go-tpm-tools.

It does not create keys. It attaches to one that already exists, usually provisioned by an attestation agent that generated it inside the TPM and had it certified.

crypto/tls asks a private key to sign the handshake transcript. The TPM signs internally and hands back the signature. The key is never read out, so there is nothing in process memory to leak.

The only input is the certificate you plan to present:

// The certificate picks the key, so there is nothing to configure.
key, err := tpmtls.OpenForCertificate(tpmtls.DefaultDevice, cert)
if err != nil {
	return err
}
defer key.Close()

cfg := &tls.Config{
	Certificates: []tls.Certificate{key.TLSCertificate(cert.Raw)},
	MinVersion:   tls.VersionTLS13,
}

The same config serves a client or a server: pass it to tls.Dial, tls.Listen, or an http.Server.

Install

go get github.com/bschaatsbergen/go-tpm-tls

The import path is hyphenated, the package is not:

import "github.com/bschaatsbergen/go-tpm-tls" // package tpmtls

Attaching to a key

Use OpenForCertificate and pass the certificate you plan to present. It opens the TPM and picks the key whose public half matches, so you do not have to configure a handle that differs from machine to machine. Use Open when you do know the handle. Both close the device when you close the key.

New and NewForCertificate are the same two over a TPM connection you already have, and they leave it open. Use them when the TPM is shared with other code in the same process, or in tests against a simulator.

The full API — signing, CSRs, finding handles — is documented at pkg.go.dev.

Notes

Use an ECDSA key. RSA keys attach and sign, but not the way crypto/tls asks: TLS wants RSA-PSS at a salt length the TPM will not use, so the handshake fails at signing time with an error that does not mention any of this.

A TPM signature costs milliseconds where a software key costs microseconds, and it lands once per full handshake rather than once per request. Session resumption and connection reuse keep it off later connections, which is usually enough to make it irrelevant.

Signing is serialized, since a TPM runs one command at a time, so what a machine is limited to is new handshakes per second, not requests per second. That limit is per machine, since each machine has its own TPM.

Where the key sits matters as much as any of this. A transient object is swapped in and out by the kernel resource manager around each command, so every signature pays to load the key back in, roughly ten times the cost of a persistent one. Measurements are in go-tpm-tls-bench.

Tests

go test ./...
golangci-lint run

Tests run against the TPM 2.0 reference simulator, so they need no TPM and no root.

License

go-tpm-tls is released under a BSD-style license. See LICENSE.

Links