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
+15 -7
View File
@@ -1077,13 +1077,21 @@ have their EK there), so enrolment could never create our key.
(`avc: denied … tpm_device_t`); the faithful path is the **initramfs** run
(reboot, unconfined), or `enforcing=0` for hand-run TPM inspection.
**FOLLOW-UP HARDENING (from the two findings above, not yet built):**
- **Content-aware NV presence check** — read NV `0x01800001`, parse as X.509, verify
it **chains to the fleet CA** (bundled `radius-ca.pem`); only then treat as enrolled,
else enrol + overwrite (`WriteNVCert` already undefine+redefines). Cleanest as a
`fleet-enrol -check-only` mode (no network) the hook calls — one content-aware source
of truth, replacing the hook's blind `tpm2_nvread` and the binary's `NVPresent`
(defined-only). Stops leftover/foreign NV from suppressing enrolment.
**FOLLOW-UP HARDENING:**
- **Content-aware NV presence check — BUILT 2026-07-03.** `fleet-enrol -check-only`
(no network / no key-gen) reads NV `0x01800001` via `tpmkey.ReadNVCert`, parses it as
X.509, checks the validity window, and — when a real fleet CA is bundled (`-cacert`,
default `radius-ca.pem`) — verifies it **chains** to it. Only a valid fleet cert counts
as enrolled (rc 0); empty/garbage/foreign/expired → rc 20 (enrol + overwrite;
`WriteNVCert` undefine+redefines). The dracut hook now calls it as the single gate,
replacing the blind `tpm2_nvread` (removed from `tpm-enrol.sh`; the binary's `NVPresent`
stays for cheap existence checks). **Lab fallback:** the shipped `radius-ca.pem` is a
*placeholder* (comments, not a cert) → `loadCAPool` returns nil → chain-less "parseable
+ unexpired" acceptance, so the reboot-converge doesn't loop on a bench-CA cert;
**production must replace `radius-ca.pem` with the real fleet CA** for strict chaining.
Tests: `classifyNVCert` unit table (valid/expired/garbage/foreign/no-CA) + `simtest`
ReadNVCert roundtrip across the 512B chunk. **BENCH-VERIFY** on a real boot: a garbage/
foreign NV now enrols instead of "normal boot".
- **De-enrol / TPM-reset procedure** for re-testing: `tpm2_nvundefine 0x01800001 -C o`
(+ `tpm2_evictcontrol -C o -c 0x81000100` for a stale *app* key — never the EK), or
BIOS "Clear TPM". Document in the bench README.