#!/usr/bin/env bash # tpm-provision.sh # # 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 0x01800100 # 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 e.g. ws-vm-a} HANDLE=${HANDLE:-0x81000100} NVINDEX=${NVINDEX:-0x01800100} 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."