Files
diskless-workstation/files/ssh-hostcert/identity.go
T
2026-07-11 16:20:21 +09:30

94 lines
3.1 KiB
Go

// 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
}