fleet-enrol: content-aware NV check (-check-only), not blind "index defined"
The enrol trigger keyed on "is NV 0x01800001 defined/readable", so leftover state (an earlier tpm-provision run — bench-seen on the 820: "cert present — normal boot" with no real identity) or random bytes (tpm-selftest) silently suppressed enrolment. Now content-aware: - tpmkey.ReadNVCert reads the full NV contents (chunked). - classifyNVCert (TPM-agnostic, unit-tested): parses X.509, checks validity window, and when a real fleet CA bundle is present verifies the cert chains to it. Only a valid fleet cert = enrolled. - New `fleet-enrol -check-only` (no network/no key-gen) exits 0=valid cert, 20=enrol needed, 1=error. The full enrol path uses the same classification instead of NVPresent. - The dracut hook calls -check-only as the single gate, replacing the blind tpm2_nvread (and its now-dead TPM2TOOLS_TCTI export). tpm2_nvread stays in the initramfs for the secure path (dot1x-macsec-up.sh). Lab fallback: the shipped radius-ca.pem is a placeholder (not a cert) -> loadCAPool returns nil -> chain-less "parseable + unexpired" acceptance, so reboot-converge doesn't loop; production replaces radius-ca.pem with the real fleet CA for strict chaining. Tests: classifyNVCert table (valid/expired/garbage/foreign/no-CA) + simtest ReadNVCert roundtrip across the 512B chunk. All pass; packer validate clean.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mkCA returns a self-signed CA cert + key and a pool trusting it.
|
||||
func mkCA(t *testing.T) (*x509.Certificate, *ecdsa.PrivateKey, *x509.CertPool) {
|
||||
t.Helper()
|
||||
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: bigOne(), Subject: pkixCN("fleet CA"),
|
||||
IsCA: true, BasicConstraintsValid: true,
|
||||
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatalf("create CA: %v", err)
|
||||
}
|
||||
ca, _ := x509.ParseCertificate(der)
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(ca)
|
||||
return ca, key, pool
|
||||
}
|
||||
|
||||
// mkLeaf issues a client leaf DER signed by (caCert, caKey), valid over [nb, na].
|
||||
func mkLeaf(t *testing.T, caCert *x509.Certificate, caKey *ecdsa.PrivateKey, nb, na time.Time) []byte {
|
||||
t.Helper()
|
||||
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: bigN(0x1234), Subject: pkixCN("fleet-host"),
|
||||
NotBefore: nb, NotAfter: na,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
|
||||
if err != nil {
|
||||
t.Fatalf("create leaf: %v", err)
|
||||
}
|
||||
return der
|
||||
}
|
||||
|
||||
func TestClassifyNVCert(t *testing.T) {
|
||||
now := time.Now()
|
||||
ca, caKey, pool := mkCA(t)
|
||||
valid := mkLeaf(t, ca, caKey, now.Add(-time.Hour), now.Add(time.Hour))
|
||||
expired := mkLeaf(t, ca, caKey, now.Add(-2*time.Hour), now.Add(-time.Hour))
|
||||
|
||||
// a leaf from a DIFFERENT CA (foreign / not our fleet)
|
||||
otherCA, otherKey, _ := mkCA(t)
|
||||
foreign := mkLeaf(t, otherCA, otherKey, now.Add(-time.Hour), now.Add(time.Hour))
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
der []byte
|
||||
pool *x509.CertPool
|
||||
enrolled bool
|
||||
}{
|
||||
{"valid fleet cert chains to CA", valid, pool, true},
|
||||
{"garbage bytes are not a cert", []byte("not a certificate at all"), pool, false},
|
||||
{"expired cert triggers re-enrol", expired, pool, false},
|
||||
{"foreign cert does not chain", foreign, pool, false},
|
||||
{"no CA pool: parseable+unexpired accepted", valid, nil, true},
|
||||
{"no CA pool: expired still rejected", expired, nil, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, detail := classifyNVCert(c.der, c.pool, now)
|
||||
if got != c.enrolled {
|
||||
t.Fatalf("enrolled=%v want %v (detail: %s)", got, c.enrolled, detail)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user