94 lines
3.1 KiB
Go
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
|
|
}
|