Capstone: The Whole Identity, End to End, Then Break It
Building the Pieces
Four labs, four pieces: a chain of trust, identity bound to a subject, certificates that expire fast, and keys rooted in hardware behind an attestation gate. This lab wires them into one system and then tries to break it. By the end, a workload authenticates to a database with no shipped secret anywhere, and every attack you throw at it fails for a reason you can name.
The honesty caveats from Lab 4 still hold and one is added. SoftHSM2 and swtpm are software emulators: they reproduce the protocols faithfully, not the physical guarantees. And in this capstone the workload's TLS client key is a software key, not the TPM key, because using a TPM-resident key directly in the MariaDB client handshake needs a TPM-PKCS11 bridge that is out of scope here. The attestation proves the chip; binding the issued cert's key into that chip is the one seam software cannot close in this playground.
One more seam, since it is visible in the filesystem: because the CA and the workload are the same machine here, the sealed challenge is created once and secret.bin sits right next to it. A real enrollment service seals a fresh nonce per request and keeps the plaintext on the CA side, where the workload can never read it. The handshake you run below is the real one; its anti-replay property is the part collapsed by putting both roles on one box. Everything else runs for real.
Step 1: The CA, rooted in hardware
Build the CA whose key never becomes a file (Lab 4), plus the issuing machinery that lets it sign short-lived certs.
sudo apt-get update && sudo apt-get install -y softhsm2 opensc libengine-pkcs11-openssl tpm2-tools swtpm golang-cfssl mariadb-server
mkdir -p ~/cap && cd ~/cap
export SOFTHSM2_CONF=$HOME/cap/softhsm2.conf
export PKCS11_MODULE_PATH=/usr/lib/softhsm/libsofthsm2.so
export KEYURI="pkcs11:token=caHSM;object=caKey;type=private;pin-value=1234"
mkdir -p tokens && echo "directories.tokendir = $HOME/cap/tokens" > softhsm2.conf
softhsm2-util --init-token --free --label caHSM --pin 1234 --so-pin 5678
pkcs11-tool --module "$PKCS11_MODULE_PATH" -l --pin 1234 --keypairgen --key-type EC:prime256v1 --label caKey --id 01
openssl req -new -x509 -engine pkcs11 -keyform engine -key "$KEYURI" -subj "/CN=HSM Root CA" -days 3650 -sha256 -out ca-hsm.pem
Set up the issuing database. The one non-obvious setting is unique_subject = no: renewal re-issues the same subject constantly, and without this the CA refuses the second one.
cd ~/cap
: > index.txt && echo 1000 > serial
echo "unique_subject = no" > index.txt.attr
cat > openssl.cnf <<'CNF'
[ca]
default_ca = CA_default
[CA_default]
dir = .
database = $dir/index.txt
new_certs_dir = $dir
serial = $dir/serial
default_md = sha256
policy = pol_any
copy_extensions = none
[pol_any]
commonName = supplied
[server_ext]
extendedKeyUsage = serverAuth
subjectAltName = DNS:db.local,IP:127.0.0.1
[client_ext]
extendedKeyUsage = clientAuth
CNF
Check it. The CA cert exists and its key is not on disk:
openssl x509 -in ca-hsm.pem -noout -subject
ls ca-hsm-key.pem 2>/dev/null && echo "BAD: key on disk" || echo "OK: CA key is only in the HSM"
Step 2: The workload's TPM and the attestation challenge
Start the workload's TPM, create its endorsement and attestation keys (Lab 4), and have the CA seal a challenge that only this TPM can open.
cd ~/cap
swtpm socket --tpm2 --tpmstate dir=$HOME/cap --ctrl type=tcp,port=2322 --server type=tcp,port=2321 --flags not-need-init --daemon
export TPM2TOOLS_TCTI="swtpm:host=127.0.0.1,port=2321"
tpm2_startup -c
tpm2_createek -c 0x81010001 -G ecc -u ek.pub
tpm2_createak -C 0x81010001 -c ak.ctx -G ecc -u ak.pub -n ak.name
tpm2_evictcontrol -c ak.ctx 0x81010002
tpm2_flushcontext -t && tpm2_flushcontext -l
# CA side: seal a challenge to billing's TPM (its EK + AK name)
NAME=$(od -An -v -tx1 ak.name | tr -d ' \n')
head -c 16 /dev/urandom > secret.bin
tpm2_makecredential -T none -e ek.pub -s secret.bin -n "$NAME" -o cred.out
Check it. The challenge file exists; only the real TPM can open it, which is what the enrollment script will require:
ls -l cred.out
Step 3: The database, trusting only the hardware CA
Issue the database its own server cert from the HSM CA, point MariaDB at the HSM CA as its only trust anchor, and create billing bound to its subject.
cd ~/cap
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout server-key.pem -out server.csr -subj "/CN=db.local" 2>/dev/null
openssl ca -batch -notext -config openssl.cnf -engine pkcs11 -keyform engine -keyfile "$KEYURI" -cert ca-hsm.pem \
-in server.csr -out server.pem -extensions server_ext \
-startdate "$(date -u +%y%m%d%H%M%SZ)" -enddate "$(date -u -d '+1 day' +%y%m%d%H%M%SZ)"
sudo mkdir -p /etc/mysql/ssl
sudo cp ca-hsm.pem server.pem /etc/mysql/ssl/ && sudo cp server-key.pem /etc/mysql/ssl/
sudo chown -R mysql:mysql /etc/mysql/ssl && sudo chmod 640 /etc/mysql/ssl/server-key.pem
sudo tee /etc/mysql/mariadb.conf.d/99-ssl.cnf >/dev/null <<'CNF'
[mariadbd]
ssl-ca = /etc/mysql/ssl/ca-hsm.pem
ssl-cert = /etc/mysql/ssl/server.pem
ssl-key = /etc/mysql/ssl/server-key.pem
CNF
sudo systemctl restart mariadb
echo "127.0.0.1 db.local" | sudo tee -a /etc/hosts >/dev/null
sudo mariadb <<'SQL'
DELETE FROM mysql.user WHERE User='';
CREATE USER 'billing'@'%' REQUIRE SUBJECT '/CN=billing.internal';
GRANT ALL PRIVILEGES ON *.* TO 'billing'@'%';
FLUSH PRIVILEGES;
SQL
Check it. The database is up and offering TLS anchored on your hardware CA:
sudo mariadb -N -e "SHOW STATUS LIKE 'Ssl_cipher';"
The Enrollment Gate
Step 4: Enrollment, the gate that ties it together
This is the whole system in one script: attest first, issue only if attestation passes. A workload that cannot prove it is billing's TPM gets no certificate.
cd ~/cap
cat > enroll.sh <<'SH'
#!/bin/sh
set -e
export SOFTHSM2_CONF=$HOME/cap/softhsm2.conf
export PKCS11_MODULE_PATH=/usr/lib/softhsm/libsofthsm2.so
export KEYURI="pkcs11:token=caHSM;object=caKey;type=private;pin-value=1234"
cd $HOME/cap
# 1. ATTEST: only billing's TPM can open the sealed challenge
fail() {
tpm2_flushcontext session.ctx >/dev/null 2>&1 || true
echo "ATTESTATION FAILED: not billing's TPM. No certificate issued."
exit 1
}
rm -f got.bin # never let a previous run's plaintext stand in for a fresh proof
tpm2_startauthsession --policy-session -S session.ctx >/dev/null 2>&1 || fail
tpm2_policysecret -S session.ctx -c endorsement >/dev/null 2>&1 || fail
tpm2_activatecredential -c 0x81010002 -C 0x81010001 -i cred.out -o got.bin \
-P "session:session.ctx" >/dev/null 2>&1 || fail
tpm2_flushcontext session.ctx >/dev/null 2>&1
cmp -s secret.bin got.bin || fail
# 2. ISSUE: short-lived, subject-bound cert from the HSM CA
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout billing-key.pem -out billing.csr -subj "/CN=billing.internal" 2>/dev/null
openssl ca -batch -notext -config openssl.cnf -engine pkcs11 -keyform engine -keyfile "$KEYURI" -cert ca-hsm.pem \
-in billing.csr -out billing.pem -extensions client_ext \
-startdate "$(date -u +%y%m%d%H%M%SZ)" -enddate "$(date -u -d '+60 sec' +%y%m%d%H%M%SZ)" 2>/dev/null
echo "ATTESTATION OK. Issued billing cert, valid 60 seconds."
SH
chmod +x enroll.sh
./enroll.sh
Now connect to the database as billing, using the freshly issued identity:
cd ~/cap
mariadb --protocol=tcp -h db.local --ssl-ca=ca-hsm.pem \
--ssl-cert=billing.pem --ssl-key=billing-key.pem \
-u billing -e "SELECT CURRENT_USER(), 'no shipped secret anywhere' AS how;"
Check it. You connected as billing@% with zero secrets pre-placed: the only thing on the workload is its TPM, and the only thing it sent was a cert it earned by proving that TPM. That is the entire course, running.
Attack It
Step 5: Attack it
Attack 1, wait it out. Do nothing for a minute, then reuse the same cert:
sleep 65
mariadb --protocol=tcp -h db.local --ssl-ca=ca-hsm.pem \
--ssl-cert=billing.pem --ssl-key=billing-key.pem -u billing -e "SELECT 1;"
It fails with certificate expired. A copied cert is worthless after its TTL.
Attack 2, impersonate. Issue yourself a perfectly valid cert from the same CA under a different name, and try to be billing:
cd ~/cap
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout intruder-key.pem -out intruder.csr -subj "/CN=intruder.internal" 2>/dev/null
openssl ca -batch -notext -config openssl.cnf -engine pkcs11 -keyform engine -keyfile "$KEYURI" -cert ca-hsm.pem \
-in intruder.csr -out intruder.pem -extensions client_ext \
-startdate "$(date -u +%y%m%d%H%M%SZ)" -enddate "$(date -u -d '+1 day' +%y%m%d%H%M%SZ)" 2>/dev/null
mariadb --protocol=tcp -h db.local --ssl-ca=ca-hsm.pem \
--ssl-cert=intruder.pem --ssl-key=intruder-key.pem -u billing -e "SELECT 1;"
It fails with Access denied. The cert is valid and chains to the CA, but the subject is not billing's. The flatness trap stays shut.
Attack 3, steal the renewal. A thief who got onto a different box cannot re-enroll, because enrollment demands the TPM that owns the challenge. Simulate their machine with a second, empty TPM and run the same script:
cd ~/cap
mkdir -p thief # swtpm refuses to start if its state directory does not exist
swtpm socket --tpm2 --tpmstate dir=$HOME/cap/thief \
--ctrl type=tcp,port=2422 --server type=tcp,port=2421 --flags not-need-init --daemon
sleep 1
TPM2TOOLS_TCTI="swtpm:host=127.0.0.1,port=2421" tpm2_startup -c
TPM2TOOLS_TCTI="swtpm:host=127.0.0.1,port=2421" tpm2_getcap handles-persistent # empty: no EK, no AK
TPM2TOOLS_TCTI="swtpm:host=127.0.0.1,port=2421" ./enroll.sh
The tpm2_getcap line is there on purpose: it shows the thief's TPM is genuinely running but holds no persistent keys, so when enrollment fails you know it failed because the chip is wrong, not because the emulator never came up.
Check it. Enrollment prints ATTESTATION FAILED and issues nothing: the thief's TPM does not hold billing's keys, so it cannot open the challenge. Meanwhile, the legitimate workload renews fine:
./enroll.sh && mariadb --protocol=tcp -h db.local --ssl-ca=ca-hsm.pem \
--ssl-cert=billing.pem --ssl-key=billing-key.pem -u billing -e "SELECT CURRENT_USER();"
The real box re-attests and gets a fresh cert; the thief is locked out at the gate.
Recap
You built and then attacked a complete workload-identity system:
- A workload proved itself by attestation, with no pre-placed secret, killing secret zero.
- It received a short-lived, subject-bound cert from a CA whose key lives in hardware.
- Expiry made a copied cert useless, subject binding stopped impersonation, and the attestation gate stopped renewal theft from another box.
Trust bottomed out exactly where the model said it would: the HSM holding the one signing key, and each workload's TPM holding a key it can use but never export. Everything above those two facts was short-lived and re-derivable. That is the design philosophy carried the whole way through: root trust in the smallest, hardest-to-copy thing, and make everything above it ephemeral. It is also, piece for piece, the model behind SPIFFE and SPIRE, and you built it by hand.
- Previous lesson
- Hardware-Rooted Identity, Honestly: HSM, TPM, and Attestation