Files
diskless-workstation/serving/dot1x-bench/enrol/tpm-provision.sh
T
Audiobuzz 4d6f038e3c tpm-enrol: move signing key off the EK slot 0x81010001 → 0x81000100
Bench (Proxmox vTPM, host cygnus-cruiser-99) confirmed via tpm2_readpublic that
0x81010001 — the handle our design used for the fleet signing key — is the
TCG-standard RSA Endorsement Key slot: the object there is the swtpm-provisioned
EK (restricted|decrypt|adminwithpolicy, RSA-2048/AES128CFB, canonical EK
authPolicy 837197…, EK cert at NV 0x1c00002). Most firmware/swtpm TPMs occupy
that slot, so EnsureKey could never create our key there — it errored reading
the EK as ECC and enrolment died before the CSR/POST.

Move the fleet signing key to 0x81000100, a non-reserved owner persistent
handle (clear of EK 0x81010001/2 and SRK 0x81000001/2). Threaded through:
  - fleet-enrol -handle default + new rd.dot1x.handle= cmdline knob (baked by
    extract-artifacts.sh, overridable via SMBIOS)
  - files/etc/dot1x/wpa_supplicant-dot1x.conf private_key handle
  - the dracut hook, bench tpm-provision.sh / tpm-selftest.sh
  - CLAUDE.md invariant #1 + design/SSP docs + TODO #11
NV index 0x01800001 (owner range) is unchanged.

EnsureKey hardening: refuse to evict any restricted-decrypt object (EK/SRK) as
a safety net — never clobber the TPM endorsement/storage identity (also
protects the future EK/AK attestation path). Still self-heals a genuine stale
signing key. New regression TestEnsureKeyRefusesToEvictProtectedKey; the
stale-key regression and handle constants moved to 0x81000100.

Design note records the IDevID (0x81020001) option for the later EK/AK
attestation work. Follow-up items logged: content-aware NV presence check
(verify the cert chains to the fleet CA, so leftover/foreign NV can't suppress
enrolment) and a de-enrol/TPM-reset procedure.
2026-07-02 12:19:59 +00:00

81 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# tpm-provision.sh <host-id>
#
# One-shot enrolment for ONE workstation. Models the real flow (invariant #1:
# per-host identity is created at enrolment, never baked into the image):
# 1. generate a signing key INSIDE the TPM, persist it at handle 0x81000100
# (non-reserved owner handle; NOT the EK slot 0x81010001)
# 2. build a CSR using that TPM key (private key never leaves the TPM)
# 3. have step-ca sign it (token-gated 'enrol' provisioner)
# 4. write the issued cert (DER) to TPM NV index 0x01800001
# At boot the dracut hook reads the cert from NV and wpa_supplicant signs EAP-TLS
# with the persistent-handle key. hostnqn is later derived from the cert serial.
#
# Run on the workstation (booted into a provisioning env with tpm2-tools, an
# openssl TPM2 provider, and the `step` CLI) on a network that can reach step-ca.
# Works for swtpm (VMs) and a real TPM (820) — same /dev/tpmrm0 in-guest.
#
# Needs in this dir: root_ca.crt + ca-pw (copied from the serving host's
# serving/dot1x-bench/pki/{root_ca.crt,stepca/secrets/password}).
#
# *** BENCH-VERIFY (CLAUDE.md "Known bench-verify items"): the TPM key reference
# *** for openssl AND wpa_supplicant differs by distro build — OpenSSL TPM2
# *** PROVIDER (handle:0x...) vs tpm2-pkcs11 URI. This script uses the provider
# *** form; switch to the engine/pkcs11 form if your build needs it, and keep it
# *** consistent with files/etc/dot1x/wpa_supplicant-dot1x.conf.
set -euo pipefail
HOSTID=${1:?usage: tpm-provision.sh <host-id> e.g. ws-vm-a}
HANDLE=${HANDLE:-0x81000100}
NVINDEX=${NVINDEX:-0x01800001}
export TPM2TOOLS_TCTI="${TPM2TOOLS_TCTI:-device:/dev/tpmrm0}"
CA_DNS=${CA_DNS:-ca.fleet.lab}; CA_PORT=${CA_PORT:-8443}
CA_URL="https://${CA_DNS}:${CA_PORT}"
cd "$(dirname "$0")"
OUT=out; mkdir -p "$OUT"
[ -f root_ca.crt ] || { echo "missing root_ca.crt (copy from serving pki/)" >&2; exit 1; }
[ -f ca-pw ] || { echo "missing ca-pw (copy pki/stepca/secrets/password)" >&2; exit 1; }
# --- 1. persistent TPM signing key ------------------------------------------
if ! tpm2_readpublic -c "$HANDLE" >/dev/null 2>&1; then
echo "==> creating TPM signing key, persisting at $HANDLE"
tpm2_createprimary -C o -G ecc -g sha256 -c "$OUT/primary.ctx" >/dev/null
tpm2_create -C "$OUT/primary.ctx" -G ecc256:ecdsa-sha256 \
-u "$OUT/key.pub" -r "$OUT/key.priv" >/dev/null
tpm2_load -C "$OUT/primary.ctx" -u "$OUT/key.pub" -r "$OUT/key.priv" \
-c "$OUT/key.ctx" >/dev/null
tpm2_evictcontrol -C o -c "$OUT/key.ctx" "$HANDLE" >/dev/null
else
echo "==> reusing existing persistent key at $HANDLE"
fi
# --- 2. CSR signed by the TPM key (key stays in the TPM) ---------------------
echo "==> building CSR (CN=$HOSTID) with the TPM key via the openssl tpm2 provider"
openssl req -new -provider tpm2 -provider default \
-key "handle:$HANDLE" \
-subj "/O=fleet/CN=$HOSTID" \
-out "$OUT/$HOSTID.csr"
# --- 3. step-ca signs the CSR -----------------------------------------------
echo "==> requesting cert from step-ca"
TOKEN=$(step ca token "$HOSTID" --provisioner enrol \
--ca-url "$CA_URL" --root root_ca.crt \
--provisioner-password-file ca-pw --not-after 720h)
step ca sign "$OUT/$HOSTID.csr" "$OUT/$HOSTID.pem" \
--token "$TOKEN" --ca-url "$CA_URL" --root root_ca.crt
openssl x509 -in "$OUT/$HOSTID.pem" -outform DER -out "$OUT/$HOSTID.der"
# --- 4. write cert into TPM NV ----------------------------------------------
SZ=$(stat -c%s "$OUT/$HOSTID.der")
echo "==> writing $SZ-byte cert to NV $NVINDEX"
tpm2_nvundefine "$NVINDEX" -C o 2>/dev/null || true
tpm2_nvdefine "$NVINDEX" -C o -s "$SZ" -a 'ownerread|ownerwrite|authread|authwrite' >/dev/null
tpm2_nvwrite "$NVINDEX" -C o -i "$OUT/$HOSTID.der"
SERIAL=$(openssl x509 -in "$OUT/$HOSTID.pem" -serial -noout | cut -d= -f2 | tr 'A-F' 'a-f')
echo "==> enrolled $HOSTID"
echo " cert serial : $SERIAL"
echo " boot hostnqn: nqn.2026-06.org.example.fleet:host-$SERIAL"
echo " key @ $HANDLE, cert @ NV $NVINDEX — the secure-path image will read these."