The Complete Guide to TPM 2.0 Certificate Attestation
How TigerTrust cryptographically gates certificate issuance on hardware-rooted proof of device state — the mental model, the flows, the policies, the caveats.
Every certificate TigerTrust issues to a TPM-backed device is gated on three cryptographic proofs — a signed quote over firmware measurements, a manufacturer-anchored key identity, and evidence that the private key never leaves the chip. This guide walks the entire pipeline, from the AK enrollment ceremony to the per-issuance verifier, and shows how policy turns these primitives into an operational trust model.
What TPM 2.0 attestation is and why it exists {#what-is-tpm-attestation}
A Trusted Platform Module (TPM) is a small dedicated cryptoprocessor soldered onto — or firmware-integrated into — the mainboard of most modern servers, industrial controllers, and PCs. The TPM does three things that a general-purpose CPU cannot safely do on its own: it stores keys that never leave the chip, it hashes measurements of the software the machine boots into a set of Platform Configuration Registers (PCRs), and it produces signed statements about those measurements on demand. Together these primitives let a device prove, to something external, what it is running — without trusting the device's own operating system to be honest.
Remote attestation is the protocol that turns that local capability into a network-usable signal. In the classical formulation defined by the Trusted Computing Group, an appraiser (or "verifier") sends a fresh nonce to the attester (the device); the device asks its TPM to produce a signed quote over a selection of PCRs that includes the nonce as extra data; the appraiser checks the signature, the nonce, and the PCR values against a policy. If everything matches, the appraiser is willing to make a decision — issue a certificate, allow a session, unseal a secret — that depends on the device being in the state the policy describes.
TigerTrust puts this pattern in front of certificate issuance. Passwords, API keys, and even mTLS client certificates can be exfiltrated from a compromised host and used from anywhere. A TPM-gated certificate cannot: the CSR's private key lives inside the TPM, the quote proves the operating system asking for renewal is the one you shipped, and the whole exchange is bound to a fresh, single-use nonce. If the device is not in the expected state, the CA refuses to sign. There is no fallback path an attacker can take.
The reason this matters now, and not five years ago, is a combination of scale and threat model. Modern deployments issue certificates to fleets of edge gateways, industrial controllers, medical devices, and autonomous vehicles — endpoints that sit in physically untrusted environments and never get a human operator's hands on them again. At the same time, standards bodies have caught up: IEEE 802.1AR-2018 formalises hardware-anchored device identity, IETF RFC 9334 (RATS Architecture) defines the roles and evidence formats, and industrial security standards like IEC 62443-4-2 now require hardware roots of trust for many component security levels. TPM 2.0 is the widely deployed silicon primitive that makes all of this practical.
TigerTrust implements the full TCG-standard flow — TPM2_Quote, TPM2_MakeCredential/ActivateCredential, TPM2_Certify — across a Go agent on the device (services/agent), a Go PKI Core on the server (services/pki-core), and a Node.js backend that owns policy, audit, and the nonce store (apps/backend). Every issuance walks the pipeline in this guide.
The three questions every enterprise asks {#three-questions}
Whenever we take an engineer through the design for the first time, the same three questions come up. They map exactly to the three proofs the verifier requires. Skipping any one of them leaves a gap that an attacker can walk through.
1. Is the device in a state we approved?
The verifier answers this with a TPM2_Quote signed by the device's Attestation Key (AK), over the PCRs that measure firmware, boot loader, kernel, and Secure Boot state. The verifier recomputes the PCR digest from the raw PCR values the agent supplied, checks that the digest inside the signed TPMS_ATTEST structure matches, and then compares each individual PCR against the golden values recorded in the attestation policy. If PCR7 is expected to be 0x9D1543... (a specific Secure Boot configuration) and the device reports something else, issuance fails.
Skip this check and an attacker with stolen credentials can request a certificate from any machine — the CA has no idea whether it's talking to the production edge gateway or the attacker's laptop.
2. Does the signing key we're trusting really belong to this TPM?
The AK is a key the device generates itself inside the TPM. Nothing about its public key proves it came from a real TPM; anyone can generate an RSA key pair. The bind happens through Credential Activation: at first-contact enrollment, PKI Core encrypts a random secret to the Endorsement Key (EK) that the TPM manufacturer certified, wrapped with a key derived from the AK's Name. Only a TPM that holds both the exact EK the manufacturer certified and the exact AK whose Name went into MakeCredential can decrypt the wrapper and recover the secret. When the device sends the secret back, the server persists the AK as trusted for that device.
Skip this and the attacker sends a self-generated AK, signs a fake quote with it, and the quote — however cryptographically valid — is meaningless.
3. Is the CSR's private key really TPM-resident?
A device can produce a valid, fresh attestation from a real TPM and then submit a CSR whose private key lives in a file on disk. That defeats the whole point: the credential can be copied off the machine. TigerTrust closes this hole with TPM2_Certify. The agent asks the TPM to have the AK sign a statement about the signing key — its TPM2B_PUBLIC area, including its Name. The verifier checks three things: the AK signature over the certify structure is valid, the certified Name matches the bound public area the agent sent, and the RSA modulus (or ECC point) inside that public area matches the public key in the CSR.
Skip this and the attacker takes a valid quote from a real TPM, then substitutes their own file-based key into the CSR — same certificate authority, same subject, private key they control.
These three checks together are the entire contract. They are exhaustive — you cannot get a certificate out of a TigerTrust CA that runs the attestation profile without passing all three — and they are complete: every attack we know of on TPM-backed provisioning is prevented by one of the three.
Actors and data flow {#actors-and-flows}
The system has three components. Nothing about the crypto requires all three to be separate processes, but keeping them separate lets us assign responsibilities cleanly: the agent talks to hardware, the backend owns policy and mutable state, and PKI Core is the pure verifier plus CA.
┌─────────────────────┐ ┌─────────────────┐ ┌────────────────────┐
│ Device / Agent │ │ Node.js Backend │ │ PKI Core │
│ (services/agent) │ │ (apps/backend) │ │ (services/pki-core)│
│ │ │ │ │ │
│ • go-tpm client │ │ • Policy CRUD │ │ • Verifier │
│ • TPMProvider │ │ • Nonce store │ │ • Issuance gate │
│ • TPM2_Quote │ │ • Audit trail │ │ • CA operations │
│ • TPM2_Certify │ │ │ │ │
│ • ActivateCred │ │ │ │ │
└─────────────────────┘ └─────────────────┘ └────────────────────┘
agent traffic ──────► HTTPS/API keys ─────► HTTPS/mTLS
The agent runs on the device and talks to /dev/tpmrm0 via github.com/google/go-tpm. It never sees the plaintext AK or EK private key — those never leave the TPM. The relevant primitives are Quote, Certify, ActivateAKCredential, SealToPCRs, and MultiBankQuote, all implemented in services/agent/internal/keyprovider/tpm_provider.go.
The backend owns three Postgres tables: attestation_policies (defined in apps/shared/schema.ts), attestation_nonces (single-use, TTL-bounded), and enrolled_aks (which AKs have completed credential activation). It gates the POST /api/iot/devices/:id/attest-and-provision endpoint on policy evaluation before forwarding to PKI Core.
PKI Core hosts the actual verifier in services/pki-core/internal/attestation. It refuses to sign any CSR whose evidence bundle fails checks. The verifier is a plain Go package with no I/O — it is unit-testable against github.com/google/go-tpm-tools/simulator, and every test in the repo runs against that simulator. There is one HTTP entry point per operation, at services/pki-core/internal/api/attestation_sign.go and services/pki-core/internal/api/enrollment.go.
One-time: AK enrollment via Credential Activation
The enrollment flow runs the first time we ever see a device. Its job is to cryptographically bind the Attestation Key to the Endorsement Key the TPM manufacturer certified, so that on every future issuance we can accept the AK's signature as speaking on behalf of that specific piece of silicon.
agent pki-core
│ │
│ POST /attestation/enroll/challenge │
│ { ekCertificatePem, akPublicAreaBase64 } ─► │
│ │ parse EK cert → EK pub
│ │ chain-verify against
│ │ manufacturer trust store
│ │ decode TPM2B_PUBLIC → AK Name
│ │ gen random 32B secret
│ │ credactivation.Generate(
│ │ akName, ekPub,
│ │ symBlockSize, secret)
│ │ → (credBlob, encSecret)
│ ◄── { challengeId, │ store SHA256(secret)
│ credentialBlobBase64, │ + AK area + EK cert
│ encryptedSecretBase64 } │ (TTL 5min)
│ │
│ TPM2_ActivateCredential( │
│ AK.Handle, EK.Handle, │
│ credBlob, encSecret) │
│ → secret │
│ │
│ POST /attestation/enroll/response │
│ { challengeId, recoveredSecretBase64 } ───► │
│ │ SHA256(recovered) == stored?
│ ◄── { enrolled: true, ... } │ yes → persist enrolled AK
│ │ no → 403 enrollment_failed
Why this works: the credential blob can only be decrypted by whoever holds the EK, and the wrapped secret is HMAC'd with a key derived from the AK's Name. Only a TPM that holds both the exact EK the manufacturer certified and the exact AK whose Name went into MakeCredential can recover the secret. There is no way to fake this externally — the standard is designed such that neither key alone is sufficient.
The relevant files are services/pki-core/internal/attestation/enrollment.go (verifier), services/agent/internal/keyprovider/tpm_provider.go → ActivateAKCredential (agent), services/pki-core/internal/api/enrollment.go (HTTP), with an end-to-end test at services/pki-core/internal/attestation/enrollment_test.go::TestCredentialActivation_E2E.
Per-issuance: attested certificate
Runs every time the device wants a new certificate. AK is already enrolled — the server already trusts it as speaking for that TPM.
1. Agent asks backend for a fresh challenge nonce
POST /api/iot/devices/:id/attestation/challenge
→ { nonce (hex), expiresAt, purpose }
2. Agent produces the evidence bundle inside the TPM
─ pcrValues = tpm2.PCRRead(SHA256, [0,1,2,3,4,7])
─ quote, quoteSig = ak.Quote(pcrSel, nonce)
─ certifyInfo, certifySig = tpm2.Certify(
signingKey.Handle, ak.Handle, nonce)
─ boundArea = signingKey.PublicArea().Encode()
─ csr = x509.CreateCertificateRequest(subject, signingKeySigner)
3. Agent POSTs everything to backend
POST /api/iot/devices/:id/attest-and-provision
{ caId, templateId, csr,
evidence: { akPub, ekCert, quote, quoteSig, pcrValues,
certifyInfo, certifySig, boundPubPem,
boundPublicArea, nonce, ... } }
4. Backend consumes the nonce (rejects reused/expired)
Backend resolves the matching attestation_policy for device.deviceType
Backend forwards to PKI Core:
POST /api/v1/certificates/sign-attested
{ caId, templateId, csr, evidence, policy }
5. Verifier runs every check
─ AK signature on TPMS_ATTEST (RSA-PKCS1v15 / RSA-PSS / ECDSA)
─ ExtraData == nonce (freshness)
─ PCRDigest == sha256(concat(pcrValues)) (PCR binding)
─ Expected PCR values match policy (state check)
─ TPM2_Certify sig valid (AK signed it) (key binding, part 1)
─ CertifyInfo.Name == boundPublicArea.Name() (key binding, part 2)
─ boundPublicArea.RSA/ECC == boundPubPem (key binding, part 3)
─ EK cert chains to manufacturer trust store (identity)
─ EK manufacturer in policy allow list
6. On any failure → 403 attestation_failed
On success → CA signs the CSR, cert returned
Result recorded in device_attestations either way (audit trail)
Files: verifier at services/pki-core/internal/attestation/verifier.go, HTTP at services/pki-core/internal/api/attestation_sign.go, Node gate at apps/backend/modules/iot.ts, policy CRUD at apps/backend/modules/attestation.ts, and an end-to-end test at services/pki-core/internal/attestation/e2e_test.go covering both the happy path and every rejection reason.
Verifier internals and failure codes {#verifier-internals}
The verifier is a pure function: Verify(evidence, policy) (*Result, error). It has no I/O, no timers, no side effects. That property is deliberate — it means every rejection reason is deterministic, every path is unit-testable, and the same code runs against the TPM simulator in CI as runs against real silicon in production.
The Result struct returns per-check pass/fail. Issuance is gated on Result.Passed, and every failure carries a machine-readable reason string so operators can debug denials without reading server logs. The failure codes are stable — they're part of the operational contract with the SRE team.
| Failure code | Meaning |
|---|---|
quote_verify: AK signature invalid | AK didn't sign the quoted TPMS_ATTEST |
quote_verify: nonce mismatch | Replay attack or agent bug |
quote_verify: pcr digest does not match claimed PCR values | Agent lied about PCR contents |
pcr_missing:<n> | Required PCR not included in quote |
pcr_mismatch:<n> | PCR value differs from policy expectation |
secure_boot_not_measured | PCR7 is zero or absent |
certify_verify: certify name digest does not match bound public area | Bound key doesn't correspond to attested key |
certify_verify: bound public key modulus/exponent does not match | Attempted CSR-key swap |
ek_chain: ... | EK cert fails to chain to a trusted manufacturer root |
ek_manufacturer_not_allowed:<mfr> | EK is signed by a manufacturer not in policy |
nonce_expired / nonce_already_used / nonce_device_mismatch | Freshness violation |
Some of these deserve elaboration.
quote_verify: pcr digest does not match claimed PCR values catches an agent that submits doctored raw PCR values but forgets to also forge a matching digest inside the signed quote. Since the digest is inside the signed structure and the AK signature covers the whole thing, forging both requires an AK signing oracle — which is exactly what Credential Activation ensures the attacker doesn't have.
certify_verify: bound public key modulus/exponent does not match is the specific rejection for a CSR-key swap attack. The agent submits a valid TPM2_Certify structure for key A but a CSR carrying key B. Comparing the RSA modulus (or ECC curve+point) inside the certified TPM2B_PUBLIC against the CSR's public key catches this.
nonce_device_mismatch protects against a device that captures a valid nonce meant for a peer and tries to complete a quote using its own AK. The backend records which device requested each nonce and refuses to accept it under any other device's identity.
The verifier deliberately does not try to interpret unknown PCR values, guess kernel versions from unstructured boot data, or infer anything not backed by a signature. Everything it reports is derived from a cryptographically signed statement or from the policy.
Manufacturer trust store {#manufacturer-trust-store}
Credential Activation is only as trustworthy as the EK certificate at the root of it. If any random self-signed CA can vouch for an EK, an attacker can generate their own "TPM" in software and pass enrollment. To prevent that, the verifier optionally chain-verifies the EK certificate against a directory of TPM manufacturer roots, implemented in services/pki-core/internal/attestation/ek_truststore.go.
The directory is set via pki-core config:
server: ek_trust_bundle_dir: /etc/tigertrust/ek-roots
Any *.pem, *.crt, or *.cer file in the directory is loaded at startup. TPM vendors distribute their roots under their own licences (some require registration, some are freely mirrored), so we do not vendor them into the repository — the operator drops them into the directory during deployment.
| Manufacturer | Where to fetch |
|---|---|
| Intel PTT | https://ekop.intel.com/ekcertservice |
| Infineon SLB | https://pki.infineon.com/ |
| STMicro ST33 | https://sw-center.st.com/STSAFE/ |
| Nuvoton NPCT | https://www.nuvoton.com/security/NTC-TPM-EK-Cert/ |
| AMD fTPM | https://ftpm.amd.com/ |
| IBM | https://www.ibm.com/support/pages/tpm-endorsement-certificate-check-tool |
| Microsoft Virtual TPM | Available via TPM.msc export |
If the directory is empty or missing, the verifier logs a warning and skips chain checks — this makes local development against the simulator painless. Quote, Certify, and PCR-policy checks still run; only the manufacturer-anchored identity check is skipped. In production you always want the trust bundle populated, and the allowedEkManufacturers policy field to be non-empty.
Policy model {#policy-model}
The verifier itself is stateless. All the operational decisions — which PCRs to require, which values to accept, which manufacturers to trust — live in attestation_policies rows, defined in apps/shared/schema.ts. Each policy is matched by deviceType, and a null device type means the policy applies as a catch-all.
{ name: "prod-edge-gateway", deviceType: "edge-gateway", // null ⇒ applies to any device type expectedPcrs: { // hex-encoded digests "SHA256": { "0": "ab34…", "7": "9d15…" // BIOS + Secure Boot state } }, requiredPcrs: [0, 1, 2, 3, 4, 7], allowedEkManufacturers: ["Intel", "Infineon"], requireBoundCsrKey: true, // enforce TPM2_Certify requireSecureBoot: true, // enforce non-zero PCR7 maxAttestationAgeSeconds: 300, // freshness ceiling enabled: true }
Policies are managed through POST/GET/PATCH/DELETE /api/attestation/policies and edited in the dashboard UI at apps/dashboard/src/pages/iot-attestation.tsx.
Golden PCR capture. Expected PCR values are gathered from a known-good device using the standard TPM tooling:
$ tpm2_pcrread sha256 sha256: 0 : 0xAB34C1... 7 : 0x9D1543...
The values are copied into the expectedPcrs map, one digest per PCR index per bank. A policy can specify only the PCRs it cares about; missing PCRs are treated as unconstrained. In practice you always want to pin at least PCR0/1/2/3 (firmware) and PCR7 (Secure Boot state).
Allowed manufacturers. allowedEkManufacturers gates which TPM vendors can enroll under this policy at all. Set to ["Intel"] to accept only Intel PTT, or leave broader (["Intel", "Infineon", "Nuvoton"]) for a heterogeneous fleet. A device whose EK chains to a manufacturer not in the list fails with ek_manufacturer_not_allowed:<mfr>.
Secure Boot enforcement. requireSecureBoot: true makes a zero PCR7 a hard failure, regardless of what expectedPcrs says. This protects against devices that shipped with Secure Boot disabled and never had it turned on.
Multi-bank support. The policy can require both SHA-256 and SHA-384 quotes concurrently via the RequiredPCRBanks field. Each additional bank is verified against the same AK, and failure of any bank fails the whole attestation. This is what CNSA 2.0 and FIPS 140-3 SHA-384 profiles demand — a single-bank SHA-256 attestation would violate their algorithm mandates. See §9.8 of the design doc for the implementation.
Event log replay. For policies where fine-grained "which bootloader, which kernel signer" matters more than exact PCR pinning, the verifier can replay the UEFI event log against the quoted PCRs and reconstruct bootloader / kernel / signer state from measurement events. Policy knobs RequireSecureBootEnabled, AllowedKernelSigners, and AllowedBootloaders let you express "any kernel signed by Ubuntu" without pinning exact hashes that change every kernel update. Implementation in services/pki-core/internal/attestation/eventlog.go.
IMA measurements (PCR10). For Linux workloads with Integrity Measurement Architecture enabled, PCR10 accumulates hashes of every executable, kernel module, and configuration file loaded at runtime. TigerTrust ships a custom binary parser for the ima-ng, ima-sig, and ima-buf template families (go-tpm-tools doesn't parse PCR10), letting policy express per-path expected hashes and deny any binary not in the allow list. Failure codes: ima_unknown:<path>, ima_missing:<path>, ima_mismatch:<path> want=<x> got=<y>. Implementation in services/pki-core/internal/attestation/ima.go.
Freshness. maxAttestationAgeSeconds sets an upper bound on how old an attestation can be when re-used. This isn't a nonce lifetime — nonces are single-use and expire in 5 minutes — it's a re-attestation window for the runtime re-attestation worker described in §9.5 of the design doc.
Advanced features {#advanced-features}
The base three-question guarantee (quote / gated issuance / key binding) is the default; the features below are opt-in per policy or per issuance flow. All ten items on the original roadmap are shipped in the current version.
Event log replay (TCG EventLog)
Replays the UEFI event log against the quoted PCRs and reconstructs the boot chain — bootloader identity, kernel signer, Secure Boot authority — from the individual measurement events. This lets policy express constraints in terms of what booted rather than what specific hash resulted. The distinction matters because kernel updates change PCR values but not necessarily signer identity, so a signer-based policy tolerates routine patching while a hash-based one flags every update as a state change. Implementation: services/pki-core/internal/attestation/eventlog.go, using github.com/google/go-eventlog/tpmeventlog.ReplayAndExtract directly. Caveat: SecureBootState.Authority isn't populated by go-tpm-tools v0.4.9, so kernel-signer extraction is best-effort until upstream fills it in.
Linux IMA measurements (PCR10)
IMA extends PCR10 with hashes of every measured object. Custom binary parser handles the three common template families and produces per-path evidence the verifier can gate on. Solves the "unknown binary appeared" problem for Linux hosts — anything not on the allow list fails with ima_unknown:<path>, so a supply-chain implant that drops a new binary triggers a rejection at the next certificate renewal.
DevID (IEEE 802.1AR)
Two-tier device identity per the IEEE standard. ValidateIDevID chains a manufacturer-issued IDevID against the same trust bundle used for EKs, extracts the HardwareModuleName SAN per RFC 4108, then IssueLDevID signs a short-lived Locally-significant Device Identifier whose subject inherits from the IDevID. Solves the "how does an out-of-box device get its first credential" problem — the manufacturer's IDevID acts as the bootstrap identity, and TigerTrust issues rotatable LDevIDs from there. Files: services/pki-core/internal/devid/devid.go, endpoint POST /api/v1/devid/ldevid.
Confidential Computing attestation
Verifier accepts AMD SEV-SNP and Intel TDX quotes alongside TPM ones, so the same policy engine gates certificate issuance for workloads running inside confidential VMs where a physical TPM isn't the trust root. SGX support is reserved — the module compiles and the endpoint returns sgx_not_implemented until a DCAP integration is wired. Solves the "we run in confidential VMs, we don't have a TPM" objection. Files: services/pki-core/internal/confidential/confidential.go, dependencies github.com/google/go-sev-guest and github.com/google/go-tdx-guest.
Runtime re-attestation
Background worker sweeps every 5 minutes. Devices whose latest passing attestation is older than 2 × maxAttestationAgeSeconds are quarantined and their certificates flipped to revoked; the CRL generator picks them up on the next tick. Solves the "device was good when it enrolled six months ago, but who knows now" problem — attestation is no longer a one-time gate, it's an ongoing signal. Implementation: apps/backend/workers/runtime-reattestation.ts, wired in apps/backend/workers/index.ts.
Sealed keys
Agent primitives for TPM2_Seal and TPM2_Unseal against current PCR values via the Storage Root Key. Sealed blob is a protobuf-marshalled SealedBytes — safe to store on disk, in Git, or in any backup, and undecryptable off the original TPM in the original state. Solves the "how do we cache secrets on the edge without them being lootable if the disk is stolen" problem. Implementation: services/agent/internal/keyprovider/tpm_provider.go (SealToPCRs, Unseal).
AK certificate issuance
After Credential Activation, PKI Core signs an X.509 certificate to the AK itself. Downstream services can then validate the AK by standard certificate-path validation instead of calling TigerTrust's enrollment API for every check. Cert profile uses EKU tcg-kp-AIKCertificate (2.23.133.8.3), SAN URN urn:tpm2:ak-name:sha256:<hex>, and is non-CA. Solves the interop problem for third-party attestation consumers that expect a certificate rather than a bespoke API. Endpoint: POST /api/v1/attestation/ak-certificate.
Multi-bank PCR support
Policy can require both SHA-256 and SHA-384 quotes concurrently. Each additional bank is verified with the same AK; failure of any bank fails the whole attestation. Required for CNSA 2.0 and FIPS 140-3 SHA-384 profiles, where SHA-256 alone is not an approved algorithm for many use cases. Verifier fields: Evidence.AdditionalQuotes[], Policy.RequiredPCRBanks. Agent primitive: TPMProvider.MultiBankQuote(). Failure codes: pcr_bank_missing:<bank>, bank=<bank> pcr_mismatch:<n>.
TPM policy sessions (TPM2_PolicyPCR)
CSR-signing key can be created with a PolicyPCR digest computed via trial session and PolicyGetDigest. The TPM refuses TPM2_Sign unless the current PCR values match — even a rooted OS cannot bypass this because the check happens inside the TPM, not in the operating system asking for the signature. Solves the "malware got root and asked the TPM to sign something" problem for the highest-assurance use cases. Agent primitives: GeneratePCRBoundKey(), SignWithPolicyPCR().
Automatic re-attestation on renewal
The renewal worker calls a freshness pre-flight before enqueuing any TPM-backed certificate. Stale attestation defers the renewal and records an audit event instead of proceeding, so a device that's stopped attesting successfully doesn't accidentally get a fresh certificate while nobody's looking. Helper: apps/backend/services/attestation-freshness.ts, wired in apps/backend/workers/certificate-renewal.ts.
Deployment guide {#deployment}
Agent side
Enable the resource-manager TPM device on the host — on Linux this is /dev/tpmrm0, backed by the in-kernel tpm_rm driver. The bare /dev/tpm0 works too but doesn't multiplex sessions cleanly.
Configure the agent's key provider:
keyprovider: type: tpm tpm_device_path: /dev/tpmrm0 keystore_path: /var/lib/tigertrust/tpm
The agent needs read access to the TPM manufacturer EK certificate, which most vendors publish at NV index 0x01C00002. Standard Linux TPM tooling permissions (tss group membership) are enough on Debian- and Red Hat-family distributions.
Server side
Populate the EK trust bundle directory — drop PEM-encoded manufacturer roots into whatever directory the ek_trust_bundle_dir config points at (default /etc/tigertrust/ek-roots). See the manufacturer trust store section for vendor URLs. Without this, EK chain verification is skipped and only quote / certify / PCR-policy checks run; you almost certainly want it populated in production.
Set PKI_CORE_URL in the Node backend's .env — defaults to http://localhost:8443.
Run the schema migration to add the attestation tables:
npm run db:push
This is idempotent and adds attestation_policies, attestation_nonces, enrolled_aks, plus the new columns on device_attestations.
Start the attestation-nonce-reaper worker — auto-started by apps/backend/workers/index.ts. It purges expired nonces every 15 minutes, so worst-case an expired nonce sits in the table for 15 min after TTL. That's fine for audit purposes; tighten if row count matters for your storage tier.
First-run flow
Enrollment is a one-time ceremony per device:
# On the agent host, once per device tigertrust-agent enroll-tpm --backend https://tigertrust.example.com --device-id gw-042
Server-side logs will show:
[Attestation] Nonce issued for device gw-042
[Attestation] Enrollment challenge issued
[Attestation] AK enrolled — Name=<sha256>, EK=Infineon
From then on, tigertrust-agent renew-cert runs the full attest-and-provision flow automatically — no more manual steps, no operator intervention. The agent handles nonce fetch, quote, certify, CSR generation, and upload; the server handles verification and CA signing; the certificate lands on disk (or wherever the deploy target points) and the audit record lands in device_attestations.
Threat model {#threat-model}
TigerTrust's TPM attestation defends against a specific and well-defined class of attacks. Being explicit about scope prevents both under- and over-selling the guarantees.
Defended against:
- Stolen CSR plus credentials. An attacker who steals both a device's CSR and its API credentials cannot obtain a certificate: they cannot forge a fresh TPM2_Quote with a matching PCR digest and valid AK signature, because the AK never leaves the TPM.
- Malware on the device. Cannot extract the private key from the TPM to use elsewhere; PCR values change (usually PCR9/10 for IMA-instrumented Linux) and the quote fails the golden-value check.
- CSR-swap attack. Verifier compares CSR pubkey to
boundPublicKeyPemand to the RSA/ECC fields insideboundPublicAreaand requires the AK-signed Certify to reference that exact public area. All three must line up. - AK spoofing. Credential Activation binds the AK to the manufacturer-issued EK cert. Any AK that doesn't co-reside with a legit EK cannot activate the credential and never gets enrolled.
- Nonce replay. Nonces are single-use, expire in 5 minutes, and are consumed atomically by the backend.
- Rogue TPM manufacturer. Mitigated by curating the EK trust bundle — only load roots from vendors you're prepared to trust as an identity anchor.
Out of scope (would require additional work — some of it is genuinely hard):
- Physical extraction of TPM contents (hardware attacks). TPMs are hardened but not tamper-proof at the silicon level; assume a nation-state adversary with physical access can extract an EK if given the device. Mitigation is operational: keep TPMs behind physical security appropriate to your threat model.
- Firmware rootkit that pre-dates the quote. The measurement chain starts from PCR0, so a compromised BIOS/UEFI would produce a "valid" quote as long as it lies consistently. Mitigation: policy requires known-good PCR0/1/2/3 digests corresponding to specific, audited firmware versions, and enforces that new firmware versions go through a review process before the golden values are updated.
Frequently asked questions
Yes for the ones that expose a TPM 2.0 interface with a manufacturer-signed EK. Microsoft's virtual TPM is supported (root available via TPM.msc export), and Azure Confidential VMs / GCP Shielded VMs expose compliant vTPMs. For workloads where a TPM isn't the right trust root — for instance a confidential VM running on AMD SEV-SNP or Intel TDX — TigerTrust's confidential-computing attestation path (services/pki-core/internal/confidential/confidential.go) accepts SEV-SNP and TDX quotes through the same policy engine.
Two options. If the device has an IEEE 802.1AR IDevID from the manufacturer, TigerTrust can bootstrap an LDevID via /api/v1/devid/ldevid without requiring a TPM quote — the IDevID acts as the hardware-anchored identity. If it has neither TPM nor IDevID, it falls outside the attestation profile and gets standard credential-based issuance; the operator team should be explicit that those endpoints have a weaker security posture.
The AK and EK are tied to the specific silicon. A physical TPM replacement means a new EK, which means the device has to re-enroll via Credential Activation. A re-image typically preserves the TPM but may change PCR values (new firmware, new bootloader, new kernel) — in that case the next attestation will fail against the old policy, and either the device needs to be re-approved against fresh golden values or the policy needs to be updated. The runtime re-attestation worker will revoke the existing certificate if the state stays bad past 2× maxAttestationAgeSeconds.
Two approaches. For pinned-hash policies, capture new golden PCR values from a known-good post-update device and roll them into the policy — TigerTrust supports policy versioning so you can maintain a transition window. For event-log-based policies (RequireSecureBootEnabled, AllowedKernelSigners, AllowedBootloaders), routine firmware updates that don't change signer identity don't break the policy at all — that's the whole point of that path.
In practice, a few hundred milliseconds — dominated by the TPM operations on the device (Quote and Certify each take 50-200ms on real hardware) rather than by the verifier. The verifier itself is a pure Go function with no I/O and completes in single-digit milliseconds. The nonce round-trip adds one HTTP call ahead of the main issuance. If you're issuing at high volume, consider the batching pattern where nonces are pre-fetched and consumed in parallel.
You need to trust that the EK certificate they signed corresponds to a real, unique TPM. That trust is scoped: adding a manufacturer to the allowedEkManufacturers list on a policy means "I accept devices with TPMs signed by this vendor for this class of workload." If you don't trust Nuvoton, don't load their root; devices with Nuvoton TPMs then fail with ek_manufacturer_not_allowed:Nuvoton and never enroll. The trust decision is under operator control per policy.
An attacker with the vendor signing key could mint fake EK certificates that chain to that vendor root, then generate arbitrary AKs and pass Credential Activation for them. This is a high-value, low-probability attack — TPM vendors treat these keys as HSM-backed root material. Defense-in-depth is to combine multiple mechanisms: pin specific PCR digests (an attacker with a fake TPM still needs to match measured firmware), keep the allowedEkManufacturers list narrow, and layer IMA / PCR10 policies so a software-only "TPM" cannot produce plausible runtime measurements.
A client certificate proves possession of a private key. It says nothing about where that key lives, what software is asking for it, or whether the key was generated in a real hardware root of trust. TPM attestation adds all three: TPM2_Certify proves the key lives inside a specific TPM, TPM2_Quote proves the software state at issuance time, and Credential Activation proves the TPM is real. A stolen client-cert private key is game over; a TPM-bound key can't be stolen at all, and the attestation ties issuance to the device's operational state at the moment of the request.
See it working in your environment
TigerTrust operationalises everything in this guide.