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()
}
Output:
Index ¶
- Constants
- Variables
- type Handle
- type Key
- func (k *Key) CertificateRequest(template *x509.CertificateRequest) ([]byte, error)
- func (k *Key) Close() error
- func (k *Key) Handle() Handle
- func (k *Key) NonExportable() (bool, error)
- func (k *Key) Public() crypto.PublicKey
- func (k *Key) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error)
- func (k *Key) TLSCertificate(chain ...[]byte) tls.Certificate
Examples ¶
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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())
}
Output:
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))
}
Output:
func (*Key) Close ¶
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) NonExportable ¶
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 ¶
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 ¶
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.