// identity.go — the host's mTLS identity, reused from the TPM (same key/cert as // 802.1X, the NVMe hostnqn and the agent). The private key never leaves the TPM: // we present the enrolment cert (DER read from NV 0x01800100) as the leaf and // hand crypto/tls a crypto.Signer backed by the persistent key at 0x81000100 // (tpmkey.LoadSigner). The enrol-broker's host plane verifies this chain to the // fleet CA (RequireAndVerifyClientCert) — proving "an enrolled fleet host" — and // only then signs the SSH host cert. Mirrors files/agent/identity.go. package main import ( "crypto/tls" "crypto/x509" "fmt" "os" "fleet-ssh-hostcert/tpmkey" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" ) // tpmIdentity holds the open TPM transport plus the loaded mTLS identity so the // caller can close the device when done. type tpmIdentity struct { dev *os.File cert tls.Certificate } func (id *tpmIdentity) Close() error { if id != nil && id.dev != nil { return id.dev.Close() } return nil } // loadTPMIdentity opens the TPM, loads the signing key at handle, reads the cert // from NV index, and assembles a tls.Certificate whose PrivateKey is the TPM // signer. A missing key/cert means the host isn't enrolled yet — surfaced with a // clear error so the caller can fail-open (retry later, leave sshd untouched). func loadTPMIdentity(tpmPath string, handle, nvIndex tpm2.TPMHandle) (*tpmIdentity, error) { // transport.FromReadWriter over the /dev/tpmrm0 *os.File avoids the linuxtpm // transport and its extra deps — the same lean approach fleet-enrol/agent use. f, err := os.OpenFile(tpmPath, os.O_RDWR, 0) if err != nil { return nil, fmt.Errorf("open TPM %s: %w", tpmPath, err) } tpm := transport.FromReadWriter(f) signer, err := tpmkey.LoadSigner(tpm, handle) if err != nil { f.Close() return nil, fmt.Errorf("load signing key at %#x: %w (host enrolled?)", uint32(handle), err) } der, err := tpmkey.ReadNVCert(tpm, nvIndex) if err != nil { f.Close() return nil, fmt.Errorf("read identity cert from NV %#x: %w (host enrolled?)", uint32(nvIndex), err) } leaf, err := x509.ParseCertificate(der) if err != nil { f.Close() return nil, fmt.Errorf("parse identity cert: %w", err) } return &tpmIdentity{ dev: f, cert: tls.Certificate{ Certificate: [][]byte{der}, PrivateKey: signer, // crypto.Signer — TLS signs the handshake in the TPM Leaf: leaf, }, }, nil } // mtlsClientConfig builds the mTLS config: our TPM-backed client cert + the fleet // CA as the root that verifies the broker's server cert. serverName is the // broker's DNS name (SNI + hostname verification). func mtlsClientConfig(id *tpmIdentity, caFile, serverName string) (*tls.Config, error) { pemBytes, err := os.ReadFile(caFile) if err != nil { return nil, fmt.Errorf("read fleet CA %s: %w", caFile, err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pemBytes) { return nil, fmt.Errorf("no certificates parsed from fleet CA %s", caFile) } return &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{id.cert}, RootCAs: pool, ServerName: serverName, }, nil }