Attestation CA enrollment

The verifier authenticates an agent only through a certificate issued by the attestation CA (lota-attest-ca), so each host enrolls once before it can attest. Enrollment runs the TPM 2.0 credential-activation ceremony: the CA verifies the EK certificate chains to a trusted manufacturer root, proves the AIK and that EK share one TPM, and issues a short-lived AIK certificate whose subject is the device pseudonym. The EK is presented only to the CA; verifiers never see it.

Stand up the CA with your CA key, the trusted manufacturer EK roots and a server TLS keypair (examples/enrollment/gen-ca.sh generates the CA material). Production fleets span several TPM manufacturers, so trust a pin-enforced multi-vendor bundle with -ek-root-bundle; the CA fails closed if any pinned root is missing, mismatched, or unpinned (see configs/ek-roots/README.rst for how to materialize one):

lota-attest-ca -listen :8444 \
    -ca-cert ca.crt -ca-key ca.key \
    -tls-cert tls.crt -tls-key tls.key \
    -pseudonym-key pseudonym.key \
    -ek-root-bundle /var/lib/lota/ek-roots

-ek-root <file> is still accepted and adds operator-supplied roots (for example a swtpm CA in the enrollment demo) on top of the bundle; pass either or both.

Pair the bundle with the manufacturers' EK revocation feeds: -ek-crl <file> (repeatable) loads a manufacturer CRL, enrollment rejects a revoked EK, and SIGHUP reloads a rewritten feed in place. See the "Manufacturer CRLs" section of configs/ek-roots/README.rst.

CA signing key

The CA signing key is the anchor every issued AIK certificate chains to, so the CA loads it as a crypto.Signer and checks the signer's public key against the CA certificate at startup -- a key that does not match the certificate is refused, whether it comes from a file or an external store.

The source is selectable: an on-disk PKCS#8 PEM (-ca-key) for development, or an external key store that never exposes the private key for production. The sections below cover the production options.

-ca-key is a development-only fallback. The key sits in the clear on the host, so a host compromise yields the fleet's signing root. The CA logs a loud warning at startup whenever it is used, and it is never the production default: a production CA holds the key in an HSM (next section) and keeps -ca-key for local bring-up and tests only.

CA signing key in an HSM (PKCS#11)

A production CA holds its signing key in a PKCS#11 token (an HSM, or SoftHSM for tests) so the private key never leaves the device. This needs a PKCS#11-enabled build of the CA -- the default binary is pure-Go and has no PKCS#11 support:

make BUILD_DIR=/var/tmp/lota-build attest-ca GO_TAGS=pkcs11   # cgo + PKCS#11

Point the CA at the token instead of -ca-key. The PIN is read from the environment, never a flag, so it stays out of the process argument list:

export LOTA_CA_PKCS11_PIN=...                 # token user PIN
lota-attest-ca -listen :8444 \
    -ca-cert ca.crt \
    -ca-key-pkcs11-module /usr/lib64/softhsm/libsofthsm2.so \
    -ca-key-pkcs11-token lota-ca \
    -ca-key-pkcs11-label lota-ca-key \
    -tls-cert tls.crt -tls-key tls.key \
    -pseudonym-key pseudonym.key \
    -ek-root-bundle /var/lib/lota/ek-roots

Select the key by -ca-key-pkcs11-label (CKA_LABEL) or -ca-key-pkcs11-id (CKA_ID, hex). The CA checks the token key's public key against ca.crt at startup and refuses to run on a mismatch, the same check applied to an on-disk key, so a wrong token or label cannot sign under the CA identity. -ca-key and the -ca-key-pkcs11-* flags are mutually exclusive.

The CA key ceremony, rotation, and the offline-root / online-intermediate topology are covered in ../../security/ca-key.

Materializing the EK root bundle

The bundle ships empty: the supported set is every TPM whose EK certificate chains to a root you can verify and pin, not a fixed vendor list. Build it from the platforms you actually attest -- draft a sources line from a host's EK certificate, verify each pin out of band against the vendor's published value, then materialize the bundle:

# walk the EK cert's issuer chain to the self-signed root and draft a line
sudo tpm2_nvread 0x01c00002 -o ek.der
cp configs/ek-roots/sources.example sources
scripts/lota-ek-root-pin.sh ek.der >>sources

# after verifying each pin out of band, fetch and pin the roots
scripts/lota-ek-roots-update.sh sources /var/lib/lota/ek-roots

lota-ek-root-pin.sh prints a fingerprint over what the network returned; it does not vouch for it, so confirm the pin against the vendor before the line enters sources.

See configs/ek-roots/README.rst for the full flow.

Enrolling a host

Enroll the agent once per host (the daemon then renews the certificate on its own before the TTL, default 24h, expires):

sudo lota-agent --enroll --ca-server ca.example --ca-port 8444 \
    --ca-cert tls.crt
# stores the issued certificate and the CA endpoint in the publisher
# profile the trust anchor names, under /var/lib/lota/profiles/

--ca-cert is required. Beyond verifying the TLS peer, the trust anchor is what names the publisher: the profile directory is the SHA-256 of the anchor's SubjectPublicKeyInfo, so a host answering to several publishers keeps one AIK, one certificate and one enrollment record per publisher and no publisher can correlate the host through a shared identity. The endpoint is deliberately not the identity -- an address is mutable and two publishers can share a hostname, while reissuing the CA certificate over the same key keeps the profile.

The AIK is part of the profile, not shared across it. Each publisher's enrollment provisions its own key, in its own TPM persistent slot, with its own rotation metadata and userAuth -- a shared key would be a stable handle two publishers could correlate the same machine through. The slot is taken from a bounded range (0x81010010 upwards, sized to the profile limit), recorded in the profile so a config edit cannot shift a publisher onto another one's key, and a slot already holding an object the host did not record is skipped rather than evicted. A host that runs out of the range refuses the enrollment instead of reusing a key; persistent slots are shared with everything else on the machine, so raising the ceiling is a deliberate act, not an automatic one.

The first enrollment records the CA endpoint in that profile, so the running agent renews the certificate on its own against that endpoint as it nears expiry (it re-enrolls once the cert enters its final third of validity, backing off when the CA is unreachable). The renewal is automatic whenever an endpoint is on disk, so an enrolled host needs no scheduled --reenroll. The same guided command stays available as a manual override -- before the certificate TTL expires, or after the agent rotates the AIK -- naming only the anchor that selects the profile:

sudo lota-agent --reenroll --ca-cert tls.crt

Point every verifier at the CA root:

lota-verifier -aik-ca-cert ca.crt ...

A host that has not enrolled (no AIK certificate) is refused under the production --require-cert default.

See examples/enrollment/README.rst for the full end-to-end walk-through.

Self-service re-anchor (diverse-fleet profile)

On the diverse-fleet profile (a policy with require_secureboot), a legitimate firmware update shifts PCR 0/1 and would otherwise reject the host until an operator clears its baseline. profile: consumer in the policy lets the verifier re-pin the per-device baseline itself when the drift preserves the Secure Boot root of trust (PK/KEK/db unchanged, dbx append-only, Secure Boot still on, firmware version not rolled back); a hardware platform that reports no firmware version (ESRT, common on DIY boards flashed with the vendor tool rather than a UEFI capsule) takes a Low-Firmware-Assurance path. The LFA re-anchor also applies automatically -- the player is never blocked waiting on the operator -- but it flags the device for post-fact review: list flagged devices with GET /api/v1/reanchor/review and clear one after inspecting it with POST /api/v1/clients/{clientID}/reanchor-review-ack (each LFA re-anchor is also logged at security level and counted in the lfa re-anchor metric). If a reviewed re-anchor looks wrong, revoke or ban the device through the existing endpoints.

The profile lives in the policy because it describes the fleet, not the deployment, and one verifier can serve several tenants and therefore several fleets. enterprise (and an unset profile) leaves the path off, which is right where firmware drift is a finding and re-baselining stays a deliberate operator action. --enable-self-service-reanchor remains as an override in both directions for an operator who has to contradict a policy they cannot immediately re-sign; left unset, each policy decides for itself.

Operator-forced re-anchor and client removal

Deliberate counterpart of the self-service path works on every profile and needs no verifier flag. When a platform change is legitimate but the self-service re-anchor refuses it (or the profile does not enable it), drop the device's pinned baselines through the admin API; the next attestation re-establishes trust per the active TOFU/policy configuration:

curl -X POST https://verifier:8080/api/v1/clients/{clientID}/reanchor \
    -H "Authorization: Bearer $ADMIN_KEY" \
    -d '{"actor":"ops@example.com","note":"planned firmware update"}'

actor is required and lands in the audit log together with the note; the action is also logged at security level and counted in the forced re-anchor metric. The enrollment is untouched -- the host keeps attesting with its enrolled AIK identity.

To remove a device from the fleet entirely (decommissioned host, or trust state that must be rebuilt from scratch), delete the client. This drops the baselines and any AIK-store registration; the optional body is audit metadata:

curl -X DELETE https://verifier:8080/api/v1/clients/{clientID} \
    -H "Authorization: Bearer $ADMIN_KEY" \
    -d '{"actor":"ops@example.com","note":"decommissioned"}'

Revocations and hardware bans are keyed separately and survive the delete, so removing a client cannot be used to shed either -- a revoked identity that re-enrolls is still revoked.

AIK rotation status over D-Bus

The agent rotates the AIK on its own schedule (--aik-ttl, default 30d). It surfaces the rotation state over D-Bus so an operator -- or a fleet monitor -- can see when a rotation is due and when a re-enrollment is needed. GetRotationStatus returns the generation, the AIK creation time, the next-rotation deadline, any open grace window, and whether the stored certificate has been outdated by a rotation:

busctl call org.lota.Agent1 /org/lota/Agent1 org.lota.Agent1 \
    GetRotationStatus
# (tttttb) generation provisioned_at rotation_deadline ... reenroll_required

busctl get-property org.lota.Agent1 /org/lota/Agent1 org.lota.Agent1 \
    ReenrollRequired

When ReenrollRequired is true, the host rotated its AIK and the issued certificate is stale; clear it with the guided sudo lota-agent --reenroll above. The same properties emit PropertiesChanged, so a subscriber is notified the moment a rotation happens rather than having to poll.

Assigning tenants at enrollment

A multi-tenant deployment gives each enrolled device a tenant, which the verifier then uses to scope that device's bans, revocations, baselines, logs, and PCR policy. The tenant is assigned by the CA at enrollment and written into the issued AIK certificate as a single OrganizationalUnit; the verifier reads it only after verifying the certificate chain, so a host cannot choose its own tenant.

The enterprise path resolves the tenant from an EK-to-tenant manifest. The manifest is a text file, one entry per line, mapping an endorsement-key fingerprint to a tenant name; blank lines and lines beginning with # are ignored:

# <ek_sha256> <tenant>
3b1f...c7  acme
9a20...4e  beta

The fingerprint is the SHA-256 of the endorsement key's public modulus. For an EK certificate in ek.pem an operator computes it with:

openssl x509 -in ek.pem -noout -modulus \
  | sed 's/^Modulus=//' | xxd -r -p | sha256sum

Point the CA at the manifest with --tenant-manifest:

lota-attest-ca ... --tenant-manifest /etc/lota/tenants.txt

An endorsement key with no manifest entry lands in the reserved default tenant, which is encoded as the absence of an OrganizationalUnit so single-tenant deployments keep issuing byte-identical subjects. Add --tenant-manifest-strict to refuse enrollment for any endorsement key absent from the manifest; the manifest then doubles as an EK allowlist. A strict rejection happens before the credential challenge is wrapped, so a barred device never consumes an enrollment session.

The device pseudonym in the certificate CommonName mixes in a named tenant, so one TPM enrolling into two tenants yields two distinct device IDs and never collides in the verifier's per-tenant state. A device that moves to a new tenant is therefore a new identity there and re-enrolls from scratch. The default tenant keeps the pseudonym derivation used before tenant assignment, so devices enrolled by an older CA re-enroll under the same device ID and keep their verifier-side state.

The verifier reads this tenant from the certificate and scopes the device's bans, revocations, PCR policy, and operator-API visibility to it. See ../multi-tenancy for how tenancy scopes verifier state and how to configure scoped monitoring-API access.

Assigning tenants with enrollment tokens

An EK manifest presumes the operator knows every endorsement key up front, which a consumer deployment cannot: a game title enrolls machines the operator has never seen. Those deployments hand each tenant's install flow a shared enrollment token, an opaque secret string the agent presents with its begin request; the CA resolves the tenant from the token instead of the EK.

The CA never stores tokens, only their digests. Mint a token per tenant, hand it to that tenant's distribution channel, and record its SHA-256 in a token-to-tenant file with the same shape as the manifest:

printf %s "$TOKEN" | sha256sum
# <token_sha256> <tenant>
5e88...12  game-alpha
a71c...09  game-beta

Point the CA at the file with --enrollment-tokens:

lota-attest-ca ... --enrollment-tokens /etc/lota/enroll-tokens.txt

A presented token is an explicit tenant assignment and fails closed: a token matching no entry (or any token on a CA without --enrollment-tokens) is refused with a token rejection before the credential challenge is wrapped, and never falls back to the EK manifest or the default tenant. Only a token-less enrollment takes the EK-manifest path, so both mechanisms can serve one CA. Add --require-enrollment-token to refuse token-less enrollments outright; the token set is then the sole admission control, which is the consumer shape where no enrolling EK is known in advance.

On the device, store the token in a root-only file (never on a command line, where it would be visible in ps and shell history) and point --enroll at it:

sudo install -m 600 /dev/null /etc/lota/enroll.token
printf %s "$TOKEN" | sudo tee /etc/lota/enroll.token >/dev/null
sudo lota-agent --enroll --ca-server ca.example --ca-port 8444 \
    --ca-cert /etc/lota/ca-tls.crt \
    --enroll-token-file /etc/lota/enroll.token

The token must be 1 to 128 printable, non-whitespace ASCII characters; a trailing newline in the file is tolerated. A successful enrollment persists the token (not the file path) in the profile's root-only enrollment record next to the CA endpoint, so --reenroll and the daemon's automatic certificate renewal keep presenting it without further operator input. Re-run --enroll with a new token file to replace it.

Rotate a token by minting a new one, adding its digest to the file alongside the old entry (both then enroll into the tenant), shipping the new token to the install flow, and deleting the old digest once the rollout completes. Removing a digest stops future enrollments with that token but does not revoke certificates it already produced; revoke those at the verifier.

Wire compatibility: an agent that presents no token keeps speaking protocol version 1, so upgraded agents interoperate with an old CA, and the CA answers each request in the version it arrived with, so old agents interoperate with an upgraded CA. The token travels only inside the enrollment TLS channel.