`tcb_status` alone is not interpretable. `OutOfDate` does not mean "this
host is unpatched"; it means "this host is behind whichever TCB
evaluation data the verifier happened to fetch". Intel publishes at
least two concurrently -- `update=early` runs ahead of `update=standard`
-- so two verifiers appraising the same host at the same moment can
legitimately disagree.
That is not hypothetical here. A node is judged twice by different code:
this crate (dcap-qvl against Intel PCS) decides admission, and the
mero-tee KMS (Intel Trust Authority) decides key release. On 2026-09-11
ITA reported tcbEvaluationDataNumber 22 for a Calimero node -- the
`early` collateral, which Intel began serving on 2026-08-11. This crate
requests neither `update` nor `tcbEvaluationDataNumber`, so it takes
whatever the PCS default yields. Nothing pins the two together.
The invariant those two must preserve is `policy_core` subset of
`policy_kms`: anything core admits, the KMS must be willing to serve a
key to. It currently holds only because the KMS allowlist is the wider
of the two. Tighten the KMS to `UpToDate` alone -- the stated goal --
while core evaluates against older collateral, and the failure mode is a
node that joins its group and then silently never receives a key.
Surface `tcb_evaluation_data_number` on `VerificationResult` and log it
beside the status it qualifies, so divergence is visible in logs and to
callers rather than inferred from a stuck node. It is diagnostic and
never a gate: unreadable collateral degrades to `None` rather than
failing an otherwise-good attestation, since trading a real security
check for a cosmetic one is the wrong way round.
This records the divergence; it does not yet resolve it. Pinning an
explicit baseline means changing which collateral is fetched, which
changes admission decisions, and it cannot be validated against the live
Intel PCS from a sandboxed environment. That belongs in its own change,
with the evidence this one makes available.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lnezo7uTBiJYPsoYPvRiW3
[tee-attestation] record which TCB baseline decided the verdict
Description
tcb_statuson its own is not interpretable.OutOfDatedoes not mean "this host is unpatched" — it means "this host is behind whichever TCB evaluation data the verifier happened to fetch." Intel publishes at least two concurrently:update=earlyruns ahead ofupdate=standard. So two verifiers appraising the same host at the same moment can legitimately disagree, and nothing in a log tells you which happened.That is not hypothetical for us. A node is judged twice, by different code:
dcap-qvlagainst Intel PCSOn 2026-09-11 ITA reported
tcbEvaluationDataNumber = 22for a Calimero node — theearlycollateral, which Intel began serving on 2026-08-11. This crate requests neitherupdatenortcbEvaluationDataNumber(verify.rs:CollateralClient::with_default_http(INTEL_PCS_URL)then a barefetch), so it takes whatever the PCS default yields. Nothing pins the two together, and neither side records which baseline it used.Why that matters. The invariant these two must preserve is
policy_core ⊆ policy_kms— anything core admits, the KMS must be willing to serve a key to. Today it holds only because the KMS allowlist (["uptodate", "outofdate"]) is the wider of the two. Tighten the KMS toUpToDatealone — which is the stated goal, with the exit condition already written down atDEFAULT_ALLOWED_TCB_STATUS— while core is evaluating against older collateral, and the failure mode is a node that joins its group and then silently never receives a key. No error names the cause.This PR makes the divergence visible. It deliberately does not resolve it — see Scope below.
Changes
VerificationResultgainstcb_evaluation_data_number: Option<u32>, parsed from the fetched collateral and documented with the invariant above. This fits the type's existing contract, which already states it is a report carrying "the raw material a caller needs to make a policy decision," not a verdict.OutOfDatein a log is not actionable without it.Nonefor mock quotes (no collateral is fetched) and for unparseable TCB info.serde_jsonon this crate. It was already in the workspace lock, so no new package enters the dependency graph —Cargo.lockgains one line, a back-reference.Scope — what this deliberately does not do
Pinning an explicit baseline (
update=earlyto match ITA, orstandard) is the other half. I'm not doing it here because:dcap-qvl0.5.3 hardcodesqe/identity?update=standardbut sendstcb?fmspc=…with noupdateat all, so pinning means supplying a customHttpClient— a real change to the verification path.It belongs in its own change, argued on the evidence this one makes available.
Test plan
cargo test -p calimero-tee-attestation --lib— 24 pass, including four new:reads_the_evaluation_data_number_from_collateraltcbInfodistinguishes_the_early_and_standard_baselinesunparseable_collateral_degrades_to_nonedoes_not_accept_the_signed_envelopeThat last one is the non-obvious one.
dcap-qvlstores the unwrappedtcbInfoobject, not the signed{tcbInfo, signature}envelope the PCS returns (its ownverify.rs:300deserializes the same string). If a future version stored the envelope instead, this parse would silently start returningNoneand the divergence signal would go quiet without anything failing. The test fails loudly instead.Full gate, run via
./scripts/check-like-ci.py --listrather than from memory:cargo fmt --check— cleancargo clippy --workspace --all-targets --features calimero-storage/testing -- -D warnings— clean, exit 0cargo clippy -p merod -p calimero-node -p calimero-server -p calimero-tee-attestation --all-targets --features mock-attestation -- -D warnings— clean, exit 0cargo test -p calimero-tee-attestation --features mock-attestation— 24 passcargo test -p merod --features mock-attestation— 105 + 2 passTwo disclosures rather than silent gaps:
auth-frontend/webuiover HTTPS fail TLS in this sandbox. I ran the gate withCALIMERO_AUTH_FRONTEND_SRC/CALIMERO_WEBUI_SRCpointed at empty local dirs — the documented escape hatch — rather than narrowing what I ran. No TLS verification was disabled.cargo deny check licenses sourceswas not run: cargo-deny isn't installed here. The argument that it's a no-op is thatserde_jsonis an existing workspace dependency andCargo.lockgains only a back-reference, no new package — but that is reasoning, not a run. Worth confirming in CI.Proof the fix works
This is not a bug fix with a failing reproduction — it adds a signal that did not exist. The "before" is that
tcb_statuswas logged with no way to tell which baseline produced it; the "after" is the newinfo!line carrying both, anddoes_not_accept_the_signed_envelopeis the regression test that keeps the signal from going quiet.The motivating observation is real and external: ITA reported
tcbEvaluationDataNumber = 22,attester_tcb_status = OutOfDate, advisoriesINTEL-SA-01439/INTEL-SA-01442,attester_tcb_date = 2026-02-11for a released Calimero node image on 2026-09-11.Wire contract (SDK gate)
No HTTP wire DTO or route changes.
VerificationResultis an internal Rust type, not a serialized DTO.crates/server/endpoints.json— n/a, no routes changedDocumentation update
The field carries its rationale in a doc comment, which is where a caller deciding policy will meet it. If the follow-up pins a baseline, that decision belongs in the protocol docs alongside the existing
DEFAULT_ALLOWED_TCB_STATUSreasoning — this PR doesn't make that choice, so there's nothing to state yet.🤖 Generated with Claude Code
https://claude.ai/code/session_01Lnezo7uTBiJYPsoYPvRiW3
Generated by Claude Code