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:
Audiobuzz
2026-07-03 01:02:25 +00:00
parent 4d6f038e3c
commit 86a9839cc4
7 changed files with 269 additions and 26 deletions
+31 -8
View File
@@ -12,10 +12,14 @@
// The hook then reboots into the normal secure path. Everything host-specific
// (key, cert, hostnqn) is created here at runtime — nothing is baked (invariant #1).
//
// Exit codes: 0 = enrolled (cert written to NV; caller should reboot);
// Exit codes (default/enrol mode): 0 = enrolled (cert written to NV; caller
// should reboot); 10 = a VALID fleet cert is already present (normal boot —
// caller proceeds); 1 = failure (caller leaves NV empty -> boot fails -> emergency).
//
// 10 = cert already present (normal boot — caller proceeds);
// 1 = failure (caller leaves NV empty -> boot fails -> emergency).
// Exit codes (-check-only mode, no network/no key-gen): 0 = a valid fleet cert is
// present (caller: normal boot); 20 = enrolment needed (NV empty/garbage/foreign/
// expired); 1 = error (e.g. TPM open failed). The dracut hook uses this as the
// single content-aware gate instead of a blind "is the NV index defined" check.
package main
import (
@@ -36,7 +40,10 @@ import (
"github.com/google/go-tpm/tpm2/transport"
)
const exitAlreadyEnrolled = 10
const (
exitAlreadyEnrolled = 10 // enrol mode: a valid fleet cert is already present
exitNeedEnrol = 20 // -check-only: no valid fleet cert → enrolment needed
)
func main() {
var (
@@ -46,9 +53,10 @@ func main() {
handleHex = flag.String("handle", "0x81000100", "TPM persistent handle for the signing key (non-reserved owner handle; NOT the EK slot 0x81010001)")
cn = flag.String("cn", "fleet-host", "CSR CommonName (generic; identity is the cert serial)")
iface = flag.String("iface", "", "network interface for the device-context MAC")
caFile = flag.String("cacert", "/etc/dot1x/radius-ca.pem", "CA bundle for an https broker URL")
caFile = flag.String("cacert", "/etc/dot1x/radius-ca.pem", "fleet CA bundle: verifies an https broker URL AND the NV cert's chain")
waitFor = flag.Duration("timeout", 30*time.Minute, "max wait for admin approval")
pollEvery = flag.Duration("poll", 5*time.Second, "status poll interval")
checkOnly = flag.Bool("check-only", false, "classify the NV cert and exit (no network, no key-gen): 0=valid fleet cert, 20=enrol needed, 1=error")
)
flag.Parse()
log.SetFlags(0)
@@ -75,9 +83,24 @@ func main() {
defer f.Close()
tpm := transport.FromReadWriter(f)
// Already enrolled? Then this is a normal boot; let the secure path run.
if tpmkey.NVPresent(tpm, nvIndex) {
log.Printf("cert already present at NV %s — normal boot", *nvHex)
// Content-aware enrolment decision: read the NV cert, parse it, and (when the
// fleet CA is available) verify it chains — so leftover/foreign/garbage NV does
// NOT masquerade as "enrolled" (a bare "index defined" check let an old
// tpm-provision cert or random bytes silently suppress enrolment).
enrolled, detail := nvEnrolled(tpm, nvIndex, *caFile)
if *checkOnly {
if enrolled {
log.Printf("check-only: %s — normal boot", detail)
os.Exit(0)
}
log.Printf("check-only: enrolment needed — %s", detail)
os.Exit(exitNeedEnrol)
}
// Full enrol mode: a valid fleet cert means this is a normal boot.
if enrolled {
log.Printf("%s — normal boot", detail)
os.Exit(exitAlreadyEnrolled)
}