Skip to content

TEE trust

A hosted relay, a TEE replica or an agent gate runs inside an Intel TDX trust domain (TD). Before the app sends one anything private, or signs a login that names its key, the app checks the TD’s quote itself, on the phone. A cloud API or TLS certificate does not stand in for that check. This page covers the trust pieces in com.calimero.mero.tee and the relay’s node key. They are a port of mero-js src/sealed/ and src/relay-attestation/, and they use the same fixtures.

Every check here throws TeeTrustException with the reason. A refusal is final, so retrying cannot turn it into a yes.

A TDX quote proves what runs in a TD only after four things check out:

  1. Its signature chain. The quote chains to Intel’s SGX root CA, with collateral (TCB info, QE identity and CRLs) that is valid now.
  2. The platform’s TCB status. It must be UpToDate unless you allow more. Revoked is never accepted.
  3. The image’s measurements. All five registers (MRTD and RTMR0–3) must be those of an image you trust. On GCP the MRTD only measures the shared TD firmware. The image itself is in RTMR1–3.
  4. Its report data. It must hold the nonce you sent, followed by the binding you computed: a transport key, a node key, or an agent’s keys.

The SDK checks all four with nothing but the node. The node sends the Intel-signed collateral along with its quote (includeCollateral). A node cannot forge collateral. It can only choose which valid collateral to serve.

Dcap verifies quotes with no third-party library. It is a port of verify from @phala/dcap-qvl 0.5.3, the library mero-js is handed. It makes the same checks in the same order, with the same messages:

val report: DcapVerifiedReport = Dcap.verify(quoteBytes, collateral, nowSecs)
report.status // "UpToDate", …
report.report.td10 // mrTd, rtMr0…rtMr3, reportData, tdAttributes
  • The CRLs are authenticated before any revocation answer they give is used.
  • Every certificate in every chain is checked against Intel’s root and those CRLs.
  • The TCB info and the QE identity must be signed and current.
  • The QE report must be signed by the PCK key and bound to the attestation key.
  • The quote must be signed by the attestation key.
  • The platform’s TCB level is matched and merged with the QE’s.
  • Debug enclaves and debug TDs are refused, and so are TDs with reserved attribute bits set.

DcapCollateral.fromJson reads the collateral in dcap-qvl’s form, the form /admin-api/tee/attest returns. PccsCollateral.fetch(quote) gets collateral from a PCCS for a quote whose sender includes none, such as an agent gate.

DcapVerify is a fun interface. Pass your own implementation wherever a verifier takes dcapVerify.

Each mero-tee node release publishes published-mrtds.json (the measurements of each image profile) with a cosign keyless bundle, signed by the release workflow on GitHub Actions. The SDK verifies that signature against the Sigstore trust root embedded in it (FULCIO_AUTHORITIES, REKOR_LOGS). So the file can come from anywhere, and the server it came from is never trusted:

val version = fetchNodeReleaseVersion(relayUrl) // from GET /admin-api/tee/info (a claim)
val release = fetchNodeRelease(cloudNodeReleaseUrl(DEFAULT_RELEASE_MIRROR, version))
val trusted = trustSignedRelease(release, PRODUCTION_PROFILE, minReleaseVersion = "2.3.99")
val verifier = trusted.verifier() // a QuoteVerifier over exactly that image

The verification passes only if all of these hold (verifySignedNodeRelease):

  • The signature covers the exact bytes.
  • The Fulcio certificate chains to the trust root and was valid when Rekor logged it.
  • It names NODE_RELEASE_SIGNER.
  • Rekor’s signed entry timestamp verifies, for an entry that is this signature over this file.
  • The file is a node release that lists measured profiles.

Nothing reads the clock, so a release that verifies once always does. minReleaseVersion is required. Every release ever signed stays validly signed, so without a floor a relay could present an old release with a flaw since fixed.

PRODUCTION_PROFILE is locked-read-only, the only image with no shell. The debug profiles need allowDebugProfile = true, and are for testing only.

createSignedReleaseVerifier(SignedReleaseVerifierOptions(release = { … }, minReleaseVersion)) fetches the release on each attestation. A node that restarted may run a newer one.

A VerifyTransportQuote answers whether a quote is genuine, is one you trust, and carries nonce ‖ reportDataSuffix. createQuoteVerifier builds the on-device one:

val verifier =
createQuoteVerifier(
QuoteVerifierOptions(
allowedMeasurements = trustedMeasurementsFromReleases(listOf(published), PRODUCTION_PROFILE).allowedMeasurements,
allowedTcbStatuses = listOf("UpToDate"),
),
)

allowedMeasurements names whole images, so the registers of two images never combine. allowedMrtd is accepted only together with allowedRtmr1–allowedRtmr3, because an MRTD alone does not name an image. A QuoteVerifier asks the node for collateral (includeCollateral). A verifier that returns false or throws refuses the quote.

The relay’s node key: DcapRelayKeyVerifier

Section titled “The relay’s node key: DcapRelayKeyVerifier”

A login statement names the relay node’s key. The key never comes from the relay’s own answer, because a quote cannot carry a key the TD did not bind. DcapRelayKeyVerifier is the default RelayKeyVerifier of CloudAccount and RelayLogin. It is the check mero-js’s delegated session runs:

  • the quote is verified with Dcap, against the collateral the relay sends with it;
  • its image must be the locked-read-only profile of the signed release the relay names, at MIN_RELAY_RELEASE (2.3.99) or newer;
  • its report data must be our nonce, followed by SHA-256("calimero.tee-attest.key-binding.v1" ‖ 0^32 ‖ nodeKey) (attestKeyBinding), read out of the raw quote bytes (reportDataOf).
val account = CloudAccount(store) // DcapRelayKeyVerifier() by default
when (val attempt = account.attemptRelayNodeKey(relayUrl)) {
is RelayNodeKeyAttempt.Learned -> attempt.nodeKey
is RelayNodeKeyAttempt.Unavailable -> retryLater() // e.g. the release mirror did not answer
is RelayNodeKeyAttempt.Refused -> showProblem() // the quote did not hold
}

A step that only moves bytes can fail: reading /tee/info or fetching the release. Such a failure is a RelayKeyTransportException, so the attempt is Unavailable and worth repeating. Any other failure is the verification saying no.

A mock quote (MOCK_TDX_QUOTE_V1) is accepted only from a loopback relay (a dev rig: localhost, 127.0.0.1, ::1 or the emulator’s 10.0.2.2), or with DcapRelayKeyVerifier(allowMock = true). attestRelayNodeKey(relayUrl, verify = …) runs the same checks without a CloudAccount.

Once the relay’s key is trusted, the requests themselves can be sealed to the TD: Sealed transport. For agents in a private TD, see Agent gate.