adding ssh host signing
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user