tpmtls

package module
v0.0.0-...-d165309 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

README

go-tpm-tls

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.

Documentation

Overview

Package tpmtls turns a key that already lives in a TPM into a crypto.Signer, so it can be used as the private key of a TLS certificate.

The package deliberately does not create keys. Something else owns them: typically an attestation agent that generated a key inside the TPM, bound its public half into hardware evidence, and had it certified. This package attaches to whatever that left behind.

Usually the key sits at a persistent handle, which survives reboots and is reachable by whichever process comes along later. A transient handle works just as well while the object is loaded, but only over the transport it was created on, since the kernel resource manager keeps transient handles private to the connection that made them.

What you get is proof of possession without possession: crypto/tls only ever asks a private key to sign a digest, the TPM does that internally, and the key never enters process memory. A heap dump, a core file, or a swapped page has nothing to leak, and the key cannot be copied to another machine at all. An attacker who compromises the process can use the key for as long as they have access, but they cannot take it with them.

key, err := tpmtls.Open(tpmtls.DefaultDevice, 0x81000004)
if err != nil {
	return err
}
defer key.Close()

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

Concurrency: a Key is safe for concurrent use. Signing is serialized, since a TPM processes one command at a time and TLS servers handshake in parallel.

Example

Authenticate to a server with a key the TPM will never release.

The certificate picks the key, so there is nothing to configure: whatever the agent provisioned, the public key in the certificate finds it.

package main

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"log"
	"os"

	tpmtls "github.com/bschaatsbergen/go-tpm-tls"
)

func main() {
	certPEM, err := os.ReadFile("client.crt")
	if err != nil {
		log.Fatal(err)
	}
	block, _ := pem.Decode(certPEM)
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		log.Fatal(err)
	}

	key, err := tpmtls.OpenForCertificate(tpmtls.DefaultDevice, cert)
	if err != nil {
		log.Fatal(err)
	}
	defer key.Close()

	conn, err := tls.Dial("tcp", "store.example.com:443", &tls.Config{
		MinVersion:   tls.VersionTLS13,
		Certificates: []tls.Certificate{key.TLSCertificate(block.Bytes)},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
}

Index

Examples

Constants

View Source
const DefaultDevice = "/dev/tpmrm0"

DefaultDevice is the TPM resource manager device on Linux. Prefer it over /dev/tpm0: the kernel resource manager multiplexes access, so several processes can talk to the TPM without fighting over its handle slots. Opening it usually needs root or membership of the tss group.

Variables

View Source
var ErrNotFound = errors.New("tpmtls: no persistent key matches")

ErrNotFound is returned when no persistent key in the TPM matches. Test for it with errors.Is: it usually means the agent has not provisioned a key yet, or provisioned a different one than the certificate you are holding.

Functions

This section is empty.

Types

type Handle

type Handle = tpmutil.Handle

Handle is where a key sits in the TPM. Persistent handles run from 0x81000000 to 0x81FFFFFF, and which one a key uses is decided by whoever created it.

This is an alias rather than a defined type so callers can pass handles they already have from go-tpm without converting, and can name one here without importing tpmutil themselves.

func FindHandle

func FindHandle(rw io.ReadWriter, pub crypto.PublicKey) (Handle, error)

FindHandle returns the handle of the persistent key whose public half is pub, or ErrNotFound.

This is how a workload avoids being configured with a handle. The handle a key sits at is a provisioning decision made by whoever created it, and baking that number into an application couples the application to how one machine happened to be set up. But the application already holds the certificate it is about to present, and the public key in that certificate says exactly which key in the TPM to sign with.

func PersistentHandles

func PersistentHandles(rw io.ReadWriter) ([]Handle, error)

PersistentHandles lists the persistent handles present in the TPM, for when you want to see what a machine actually holds rather than guess.

type Key

type Key struct {
	// contains filtered or unexported fields
}

Key is a key living in a TPM, exposed as a crypto.Signer so it can be used as the PrivateKey of a tls.Certificate.

Concurrency: safe for concurrent use, see the note on Sign.

func New

func New(rw io.ReadWriter, handle Handle) (*Key, error)

New attaches to the key at handle over an already open TPM transport, for when the TPM is shared with other code in the same process, or in tests against a simulator.

The handle may be persistent or transient. A transient one only resolves over the transport it was created on, so it is useful when the same process both creates and uses the key, and useless across processes.

The caller keeps ownership of rw. Close releases the reference to the key and leaves the transport open: whoever opened the descriptor closes it, since closing one that another part of the program is still using breaks that code with an error pointing nowhere near here.

Sharing a transport puts ordering on the caller. A TPM answers one command at a time, and a single file descriptor carries one exchange at a time. The kernel resource manager gives each open descriptor its own context, so separate descriptors do not interfere, but two goroutines writing to the same descriptor will interleave. Sign takes a lock, so concurrent handshakes with this Key are safe. That lock does not cover commands the caller sends over rw directly.

func NewForCertificate

func NewForCertificate(rw io.ReadWriter, cert *x509.Certificate) (*Key, error)

NewForCertificate attaches to the key matching the certificate's public key, over an already open TPM transport.

func Open

func Open(device string, handle Handle) (*Key, error)

Open attaches to the key at handle in the TPM at device.

The returned Key owns the device and closes it on Close. Nothing else in the process holds that descriptor, so every command sent over it goes through this package's lock and the caller has no ordering to arrange. Use this unless you already have a transport.

Example

Attach to a key by handle, for deployments that provision a known one.

The handle comes from configuration rather than a constant in the source. Two machines provisioned by different tooling can hold their keys at different handles, and a literal here would quietly work on one and fail on the other.

package main

import (
	"fmt"
	"log"
	"os"
	"strconv"

	tpmtls "github.com/bschaatsbergen/go-tpm-tls"
)

func main() {
	raw := os.Getenv("TPM_KEY_HANDLE") // for example 0x81000004
	handle, err := strconv.ParseUint(raw, 0, 32)
	if err != nil {
		log.Fatalf("TPM_KEY_HANDLE: %v", err)
	}

	key, err := tpmtls.Open(tpmtls.DefaultDevice, tpmtls.Handle(handle))
	if err != nil {
		log.Fatal(err)
	}
	defer key.Close()

	fmt.Printf("signing with the key at %#x\n", key.Handle())
}

func OpenForCertificate

func OpenForCertificate(device string, cert *x509.Certificate) (*Key, error)

OpenForCertificate attaches to the key matching the certificate's public key, in the TPM at device. This is usually the call you want: the certificate you are about to present picks the key, so nothing needs configuring.

cert, _ := x509.ParseCertificate(certDER)
key, err := tpmtls.OpenForCertificate(tpmtls.DefaultDevice, cert)

func (*Key) CertificateRequest

func (k *Key) CertificateRequest(template *x509.CertificateRequest) ([]byte, error)

CertificateRequest creates a DER encoded CSR for this key, signed by the TPM.

A CSR is how a certificate authority learns which public key to certify, and its self signature is what proves the requester holds the private half. Since the TPM produces that signature, the proof is as strong as the key: it can only have come from this machine.

A nil template asks for a certificate with no subject and no SANs, which is what an issuer that derives those itself expects. SPIRE is one such issuer: it names the workload from attestation and reads only the public key out of the request.

Example

Ask an issuer to certify the key.

The request carries the public half, and the signature over it, made by the TPM, is what proves this machine holds the private half.

package main

import (
	"crypto/x509"
	"fmt"
	"log"

	tpmtls "github.com/bschaatsbergen/go-tpm-tls"
)

func main() {
	key, err := tpmtls.Open(tpmtls.DefaultDevice, 0x81000004)
	if err != nil {
		log.Fatal(err)
	}
	defer key.Close()

	csr, err := key.CertificateRequest(&x509.CertificateRequest{})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d byte certificate request\n", len(csr))
}

func (*Key) Close

func (k *Key) Close() error

Close releases this reference to the key, and closes the device if Open opened it. It is safe to call more than once.

The key itself survives. A persistent handle belongs to whoever created it, and removing one takes an eviction this package deliberately does not perform: detaching from a key you did not provision should not destroy it for everyone else on the machine.

func (*Key) Handle

func (k *Key) Handle() Handle

Handle returns the handle the key is loaded at.

func (*Key) NonExportable

func (k *Key) NonExportable() (bool, error)

NonExportable reports whether the TPM will refuse to release or duplicate the private key.

It reads the attributes back from the TPM rather than trusting how the key was requested, so the answer is what the TPM enforces, not what its creator intended. Worth checking when you inherit a key and want to know what you have.

func (*Key) Public

func (k *Key) Public() crypto.PublicKey

Public returns the public half of the key.

No lock is taken: the public area was read once at load time and does not change while the key is loaded.

func (*Key) Sign

func (k *Key) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error)

Sign asks the TPM to sign digest, which is what makes this type useful to crypto/tls. The digest goes in, a signature comes back, and the private key stays where it is.

Calls are serialized. A TPM executes one command at a time, so parallel signing would either interleave on the transport or queue in the kernel anyway. Expect roughly 20ms per signature on a cloud vTPM, paid once per full TLS handshake. Session resumption and connection reuse avoid it on subsequent connections.

func (*Key) TLSCertificate

func (k *Key) TLSCertificate(chain ...[]byte) tls.Certificate

TLSCertificate pairs a certificate chain with this key, ready to drop into a tls.Config. The chain is leaf first and DER encoded, which is what crypto/tls expects.

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

Nothing here is special to TPMs. crypto/tls accepts any crypto.Signer as a private key, which is the same door PKCS#11 modules and cloud KMS keys come through.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL