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
+12 -8
View File
@@ -26,7 +26,6 @@ type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
STAMP=/run/dot1x/.enrol-done
[ -e "$STAMP" ] && exit 0
mkdir -p /run/dot1x
export TPM2TOOLS_TCTI="device:/dev/tpmrm0"
# --- skip conditions --------------------------------------------------------
if [ "$(getarg rd.fleet.enrol=)" = "0" ]; then
@@ -55,16 +54,21 @@ while [ ! -e /dev/tpmrm0 ] && [ "$i" -lt 10 ]; do sleep 1; i=$((i + 1)); done
# --- already enrolled? then this is a normal boot — do NOT touch the network -
# (Bringing up en* here would fight the secure path, which needs it UNMANAGED for
# wpa_supplicant. So gate on NV BEFORE any network action.)
if tpm2_nvread "$NVINDEX" -o /run/dot1x/nvprobe.der >/dev/null 2>&1 \
&& [ -s /run/dot1x/nvprobe.der ]; then
info "tpm-enrol: client cert present at NV $NVINDEX — normal boot"
rm -f /run/dot1x/nvprobe.der
exit 0
fi
# CONTENT-AWARE gate (fleet-enrol -check-only, no network/no key-gen): reads NV,
# parses the cert, and — when the fleet CA is bundled — verifies it chains, so a
# leftover/foreign/expired cert (or garbage) does NOT masquerade as enrolled.
# rc: 0 = valid fleet cert (normal boot); 20 = enrolment needed; other = error.
/usr/sbin/fleet-enrol -check-only -nvindex "$NVINDEX" -handle "$HANDLE"
rc=$?
case "$rc" in
0) info "tpm-enrol: valid fleet cert at NV $NVINDEX — normal boot"; exit 0 ;;
20) : ;; # fall through to enrolment
*) warn "tpm-enrol: NV check failed (rc=$rc) — leaving to the secure path"; exit 0 ;;
esac
# Committed to enrolling — one attempt per boot.
: > "$STAMP"
warn "tpm-enrol: NV $NVINDEX empty — starting FIRST-TIME TPM enrolment"
warn "tpm-enrol: no valid fleet cert in NV $NVINDEX — starting FIRST-TIME TPM enrolment"
# NIC: explicit rd.dot1x.if=, else first non-loopback, non-macsec interface.
NIC=$(getarg rd.dot1x.if=)