Skip to content

remote-attest-01: Azure confidential VM TDX evidence (azure-tdx) and Intel Trust Authority tokens (ita) - #2849

Open
tedmalone wants to merge 2 commits into
josh/acme-remote-attestfrom
ted/remote-attest-azure-tdx
Open

tedmalone wants to merge 2 commits into
josh/acme-remote-attestfrom
ted/remote-attest-azure-tdx

Conversation

@tedmalone

Copy link
Copy Markdown
Contributor

Adds an azure-tdx evidence format to remote-attest-01, for Azure confidential VMs with Intel TDX (DCesv6/ECesv6). It targets josh/acme-remote-attest.

Why

On these VMs the guest can't get a TDX quote itself. There is no /dev/tdx_guest, and configfs-tsm returns ENXIO, so the configfs-tsm evidence path doesn't work there. Instead:

  • Azure's paravisor (HCL) produces a TD report whose report data is SHA-256(runtime claims JSON), zero-padded to 64 bytes. The guest reads it from vTPM NV index 0x01400001.
  • The runtime claims carry the vTPM keys (HCLAkPub, HCLEkPub), the VM configuration, and 64 bytes of user-data the guest writes to NV 0x01400002 before reading the report.
  • IMDS (POST /acc/tdquote) turns the TD report into a normal Intel-signed v4 quote.

What it does

  • New evidence media type application/vnd.smallstep.azure-tdx+json: {"quote": <base64>, "runtimeData": <base64>}. runtimeData is the runtime claims document, byte-for-byte as the paravisor hashed it.
  • The verifier:
    • verifies the quote with the existing tdx.Verifier: signature, QE, PCK chain, collateral, TCB and non-debug;
    • requires the report data to equal SHA-256(runtimeData) ‖ 32 zero bytes;
    • returns the claims' 64-byte user-data as the bound data.
  • So the binding and claim nonce sit in user-data, in the same layout as the configfs-tsm report data. Size checks and the finalize check are unchanged.
  • Opt-in. The format is accepted only when listed in formats. The quote's MRTD measures Azure's paravisor, not the guest, and the RTMRs are zero, so a genuine quote alone says nothing about the guest's software.
  • The result carries azureRuntime alongside tdx, with the vTPM JWKs and the VM configuration (secure-boot, console-enabled, vmUniqueId, …), so webhooks and templates can apply policy.
  • tdx and azure-tdx share one TDX verifier and its roots.

Tests

  • attestation/tdx/azure_test.go verifies a real quote and runtime claims captured on a Standard_DC2es_v6 VM, since deleted, against Intel's embedded root, without collateral, at the capture time. It then checks the report-data commitment and extracts the user-data. The negative cases are:
    • a tampered claim, non-zero padding, wrong report data size;
    • bad user-data, bad JSON;
    • unknown fields and trailing data.
  • Provisioner tests cover:
    • opt-in;
    • the indicator check;
    • an invalid quote;
    • a collateral failure counted as the CA's error, not the evidence's;
    • report data that doesn't commit to the claims.
  • go test ./attestation/... ./authority/provisioner/... ./webhook/... ./acme/... passes on Windows, except acme/db/nosql. That package fails the same way without this change (badger temp-dir cleanup on Windows).

Not in this PR

  • Guest measurements. The guest image is measured only in vTPM PCRs. A measurement policy needs a vTPM quote signed by HCLAkPub (now in the result) over the chosen PCRs, ideally with the same binding as its qualifying data. That could be a follow-up field in the evidence object, or left to a webhook.
  • TCB status. go-tdx-guest refuses anything but UpToDate. An Azure DCesv6 host was rated OutOfDate by Intel Trust Authority in August 2026 and UpToDate via PCS in November 2026, so this format inherits the same exposure as tdx.
  • Client side. step-tee-plugin needs an Azure path: write binding ‖ nonce to NV 0x01400002, read the HCL report from NV 0x01400001, split out the TD report and the runtime claims, and POST the TD report to IMDS /acc/tdquote.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FfGsdwrq6AroSzEU654oya

The guest of an Azure confidential VM with Intel TDX cannot get a TDX
quote itself: there is no /dev/tdx_guest and configfs-tsm has no
provider. Its paravisor produces a TD report whose report data is
SHA-256 of a runtime claims document, zero-padded, and Azure's instance
metadata service turns that report into an Intel-signed quote. The
guest controls 64 bytes of user data in the runtime claims.

The new azure-tdx format verifies the quote with the existing TDX
verifier, checks that its report data commits to the runtime data sent
with it, and returns the claims' user data as the binding and the
relying party's nonce. The quote's MRTD measures the paravisor, not the
guest, so the format is accepted only when listed, and the runtime
claims (the vTPM's HCLAkPub key and the VM configuration) are passed to
webhooks and templates for policy.

A real quote and runtime claims captured on a deleted Standard_DC2es_v6
VM test the path against Intel's root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfGsdwrq6AroSzEU654oya
@github-actions github-actions Bot added the needs triage Waiting for discussion / prioritization by team label Nov 7, 2026
@tedmalone
tedmalone requested a review from joshdrake November 7, 2026 16:23
@tedmalone

Copy link
Copy Markdown
Contributor Author

Tested end to end on a real Azure Standard_DC4es_v6 VM, with smallstep/step-tee-plugin#2 producing the evidence and step from josh/acme-remote-attest-enroll running the order. A lab step-ca built from this branch verified the azure-tdx record with live Intel collateral (UpToDate), checked the binding at finalize, and issued a certificate for the attested key. Details are on smallstep/step-tee-plugin#2.

The new ita format accepts tokens Intel Trust Authority issues for Intel
TDX trust domains, including Azure confidential VMs whose paravisor
reports for the guest. They arrive as application/eat+jwt and are told
from Confidential Space tokens by their issuer.

step-ca verifies a token with the signing key its kid names, fetched
from Intel Trust Authority's key set at a configured URL (never the
token's jku, which must match it if present), cached and refetched for
an unknown kid. The key's x5c chain must verify to Intel Trust
Authority's root, which is embedded. The token must be recent, not for
a debuggable trust domain, and carry 64 bytes of attester_user_data:
the binding and the relying party's nonce. The runtime data its TD
quote committed to must be SHA-512 of the verifier nonce, if any, and
that user data, as Intel's client computes it, so the binding is
checked against what the quote covers rather than taken on Intel Trust
Authority's word alone.

Tested end to end on a Standard_DC4es_v6 VM with Intel Trust Authority:
a remote-attest-01 order was finalized with a token it issued.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfGsdwrq6AroSzEU654oya
@tedmalone tedmalone changed the title remote-attest-01: accept Azure confidential VM TDX evidence (azure-tdx) remote-attest-01: Azure confidential VM TDX evidence (azure-tdx) and Intel Trust Authority tokens (ita) Nov 7, 2026
@tedmalone

Copy link
Copy Markdown
Contributor Author

Added: Intel Trust Authority (ita format). Commit df35d99. Tested end to end on a real Azure TDX VM.

What it does

  • remoteAttestation.ita (roots, jwksUrl, maxTokenAge) enables an ita format.
  • ITA tokens arrive as application/eat+jwt and are routed by issuer (https://portal.trustauthority.intel.com). Anything else still goes to Confidential Space.
  • Signing keys are fetched from the configured JWKS URL, never from the token. A jku, if present, must match it. Keys are cached for 15 minutes and refetched at most once a minute for an unknown kid, which covers Intel's rotation (the key changed between August and now).
  • Each key's x5c chain must verify to Intel Trust Authority Root CA, which is embedded (valid 2024-11-27 to 2049-12-30, SHA-256 70:91:E8:…:4F:5F). Its chain is Token Signing ← ATS Signing CA ← Root. Please cross-check that root against Intel's published copy before merging.
  • PS384 or RS256 only. The token must have iss, exp, nbf and iat, be no more than 5 minutes old by default, and have tdx_is_debuggable: false.
  • Binding. The bound data is the 64 bytes of tdx.attester_user_data, i.e. binding ‖ claim nonce. tdx.attester_runtime_data["user-data"], which the TD quote committed to, must equal SHA-512(verifier_nonce.val ‖ verifier_nonce.iat ‖ user_data), with the nonce optional. That's the formula Intel's client uses. So the binding is checked against what the quote covers, not only against Intel's signature.
  • A failure to fetch keys is the CA's error, not the evidence's.
  • The claims go to webhooks and templates as .Attestation.ita.

Tested on the VM (Standard_DC4es_v6, 2026-11-07, live Intel Trust Authority):

  1. step-tee-plugin with an ITA API key (smallstep/step-tee-plugin#2) produced a token: PS384, the current 5ac4… key, appraisal: {method: azure}, TCB UpToDate, not debuggable. attester_user_data equals binding ‖ nonce, and the runtime user-data equals SHA-512 of it.
  2. A full step ca certificate --remote-attestation-plugin tee order finalized against a lab step-ca limited to formats: ["ita"]. The certificate's key is the attested key.
  3. The azure-tdx route was run again with the same binaries and still issued.

Not yet: ITA can also appraise the vTPM (tpm.pcrs in the token) when the request includes a vTPM quote. The plugin sends only TDX evidence for now, so these tokens carry no PCRs. Adding that is the next step for policy on guest measurements.

Unit tests cover the ita package (chain, rotation, cache, jku, algorithm, expiry, age, debug, both binding formulas, fetch failures) and the provisioner routing. TestAWS_authorizeToken/fail/instance-age fails on Windows on this branch with or without these changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs triage Waiting for discussion / prioritization by team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant