Skip to content

feat: use complete TDINFO hash for TCB mappings - #1032

Open
haitaohuang wants to merge 5 commits into
intel:mainfrom
haitaohuang:tcbmapping
Open

haitaohuang wants to merge 5 commits into
intel:mainfrom
haitaohuang:tcbmapping

Conversation

@haitaohuang

@haitaohuang haitaohuang commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implement the one-hash TCB mapping design proposed in #908:

  • use SHA384(TDINFO) over the complete unmasked TDINFO as the mapping key;
  • break the RTMR2 circular dependency by measuring canonical policyData with only servtdTcbMapping redacted;
  • verify the signed mapping with the RTMR1-bound policy issuer chain;
  • authenticate migration and rebinding continuity through SERVTD_EXT rather than host-supplied Init_TDINFO;
  • resolve source initial/current hashes through the authenticated source JSON mapping and reject lookup misses, SVN rollback, and nonzero SERVTD_ATTR;
  • preserve cumulative mapping history in migtd-hash, including multiple release hashes assigned to the same SVN.

The legacy outer policy signature is ignored because policy integrity is established by RTMR2.

Related to #908.

Validation

  • format, cargo check, clippy, cargo-deny, library build, and library tests;
  • all 32 firmware build combinations and all six standalone tool builds;
  • all 14 emulation workflow scenarios;
  • all five workflows triggered on the fork branch: Format and Clippy, Library Crates, Fuzzing Test, main, and Integration (Emulation Mode).

Future changes

Potential follow-up work, intentionally excluded from this focused series:

  • replace raw policy-chain measurement with a stable RTMR1 trust anchor based on a root certificate and signer-purpose EKU;
  • support direct trust-anchor enrollment and signer revocation through CRLs;
  • support CoRIM TCB endorsements, including policies that use CoRIM without JSON servTD collateral;
  • make servTD identity collateral optional where SVN-only evaluation is sufficient;
  • add release tooling and coverage for independent policy, mapping, and identity signer rotation;
  • strengthen two-phase measurement generation and hash-stability checks across release workflows.

@jyao1

jyao1 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

@haitaohuang , is any Microsoft people can review and approve as well?

Comment thread src/migtd/src/spdm/spdm_rsp.rs Outdated
Comment thread src/migtd/src/ratls/server_client.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical issuer-chain binding and TDINFO hashing issues block approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adopts complete TDINFO-hash TCB mappings, canonical RTMR2 policy measurement, and authenticated migration continuity through SERVTD_EXT.

Changes:

  • Updates policy verification, migration/rebinding, SPDM, and RA-TLS flows.
  • Updates cumulative mapping and collateral-generation tools.
  • Refreshes fixtures, templates, and build workflows.
File summaries
File Summary
tools/servtd-collateral-generator/src/main.rs Updated collateral-generator CLI flow.
tools/servtd-collateral-generator/src/build.rs nit, 2 votes: documentation still passes the removed --mapping-chain option.
tools/servtd-collateral-generator/readme.md Updated generator usage documentation.
tools/migtd-hash/src/main.rs moderate, 2 votes: invalid option combinations can panic instead of returning a validation error.
tools/migtd-hash/src/lib.rs moderate, 2 votes: accept both tdinfo_hash and tdinfoHash; reject conflicting duplicate hash/SVN assignments instead of overwriting.
tools/migtd-hash/Cargo.toml Updated hash-tool package metadata.
src/policy/test/policy_v2/tcb_mapping.json Updated TCB mapping fixture.
src/policy/test/policy_v2/servtd_collateral.json Updated collateral fixture.
src/policy/test/policy_v2/cert_chain/policy_issuer_chain.pem Updated policy issuer-chain fixture.
src/policy/src/v2/servtd_collateral.rs critical, 1 vote: the 48-byte servtd_hash is omitted from TDINFO hashing, allowing mismatches or collisions.
src/policy/src/v2/policy.rs critical, 2 votes: the supplied issuer chain is not bound to the RTMR1-measured signer event. nit, 1 vote: documentation still describes the ignored outer signature as authoritative.
src/policy/src/v2/mod.rs Exports policy-v2 measurement helpers.
src/policy/src/v2/measurement.rs Adds canonical redacted policy measurement.
src/policy/src/lib.rs Integrates policy measurement support.
src/migtd/src/spdm/spdm_rsp.rs Updates SPDM attestation response handling.
src/migtd/src/ratls/server_client.rs Updates policy and SERVTD_EXT binding.
src/migtd/src/migration/session.rs Adds migration attribute validation.
src/migtd/src/migration/rebinding.rs Updates rebinding continuity handling.
src/migtd/src/migration/mod.rs Preserves Init_TDINFO framing compatibility.
src/migtd/src/mig_policy.rs Updates mapping lookup and continuity validation.
src/migtd/src/lib.rs Updates policy helper exports.
src/migtd/src/event_log.rs Updates policy measurement event handling.
src/migtd/src/bin/migtd/main.rs Updates runtime policy measurements.
sh_script/build_policy_v2.sh nit, 1 vote: documented invocation lacks the required cumulative mapping argument.
sh_script/build_AzCVMEmu_policy_and_test.sh Updates emulation policy generation.
sh_script/Azure/build_azure_mock_test.sh moderate, 1 vote: migtd-hash build failures can be masked and omitted from binary verification.
deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdx_emu.rs Adds emulated SERVTD_EXT fields.
config/templates/td_identity.json Updated TD identity template.
config/templates/td_identity_signed.json Updated signed TD identity template.
config/templates/tcb_mapping.json Updated TCB mapping template.
config/templates/tcb_mapping_signed.json Updated signed mapping template.
config/templates/servtd_collateral.json Updated collateral template.
config/templates/policy_issuer_chain.pem Updated issuer-chain template.
config/AzCVMEmu/tcb_mapping.json Updated emulation mapping fixture.
Cargo.lock Updated dependency lockfile.
Review details

Suppressed comments (3)

sh_script/Azure/build_azure_mock_test.sh:379

  • This new build is piped through grep ... || true, but migtd-hash is omitted from the existence check below. A compiler failure can therefore be reported as “All tools built successfully” and only surface later when the missing binary is invoked at line 464. Make this build fail fast or include the binary in the verification loop.
cargo build --release -p migtd-hash 2>&1 | grep -E "(Compiling|Finished|error)" || true

sh_script/build_policy_v2.sh:8

  • The new required mapping argument makes the documented command in doc/policy_v2.md:126 fail with the usage error because it still passes only the environment. Update that guide (and any downstream invocation) to pass the cumulative mapping path rather than leaving the release workflow unusable as documented.
tcb_mapping_file="${2:-}"

src/policy/src/v2/policy.rs:213

  • This change makes the outer policy signature legacy and ignored, but doc/policy_v2.md:63-70 still instructs users to sign policyData and says the output contains an authoritative signature. That guide now describes the wrong trust model and release artifact; update it to document RTMR2's canonical measurement and the policy-issuer-signed TCB mapping instead.
    /// Legacy outer signature, ignored because policyData integrity is
    /// established by the RTMR2 measurement.
    #[serde(default)]
    pub signature: Option<String>,
  • Files reviewed: 34/39 changed files
  • Comments generated: 6
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/policy/src/v2/policy.rs
Comment thread src/policy/src/v2/servtd_collateral.rs Outdated
Comment thread tools/migtd-hash/src/lib.rs
Comment thread tools/migtd-hash/src/lib.rs
Comment thread tools/migtd-hash/src/main.rs Outdated
Comment thread tools/servtd-collateral-generator/src/build.rs
Comment thread src/policy/src/v2/policy.rs Outdated
Comment thread src/migtd/src/mig_policy.rs Outdated
Comment thread tools/migtd-hash/src/main.rs Outdated
Comment thread tools/migtd-hash/src/main.rs Outdated
@haitaohuang
haitaohuang force-pushed the tcbmapping branch 2 times, most recently from aa9fc2d to bd32692 Compare September 11, 2026 20:04
@haitaohuang

Copy link
Copy Markdown
Contributor Author

@haitaohuang , is any Microsoft people can review and approve as well?

@mingweishih

Comment thread sh_script/build_policy_v2.sh

@mingweishih mingweishih left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — one-hash TDINFO / TCB mapping

Reviewed the full branch diff at ae57d92 against merge-base 6913a90, with local context rather than hunks alone. All CI green; main has only advanced by three Dependabot bumps, none touching these files.

Overall

The design is sound and this is a real improvement. Keying the mapping on SHA384(unmasked TDINFO), breaking the RTMR2 circular dependency by redacting only servtdTcbMapping, and dropping the outer policy signature in favour of the RTMR2 measurement is a coherent argument: a host-tampered policy changes RTMR2 -> changes tdinfo_hash -> absent from the signed mapping -> denied.

Specific things I checked and found correct:

  • Canonicalization (src/policy/src/v2/measurement.rs) is hand-rolled rather than leaning on serde_json::to_vec ordering. That is the right call, because serde_json/preserve_order is enabled elsewhere in the workspace and feature unification would otherwise make the measurement depend on which crates are in the build. Sorted keys, no whitespace, array order preserved. Recursion depth is bounded because unbounded_depth is off, so serde_json's 128-level limit applies.
  • Runtime and migtd-hash share one implementation of extract_canonical_policy_data_bytes (tools/migtd-hash/src/lib.rs vs src/bin/migtd/main.rs). A divergent offline simulator would be a silent and catastrophic failure mode; sharing it removes that class of bug entirely.
  • enforce_mandatory_deny fails closed when migtd_tcb_status is None, so a tdinfo_hash absent from the signed mapping is denied rather than defaulted.
  • TdTcbMapping::validate() rejects wrong-length digests and rejects the same hash mapped to differing isvsvn, which closes the first-match ambiguity in get_engine_svn_by_measurements.
  • get_tcb_level_by_svn floor-matching returns None below all levels, i.e. denied.
  • verify_init_servtd_svn_order is called unconditionally, whereas the code it replaces was gated by cfg(not(any(AzCVMEmu, test_mock_report, use-mock-quote))). Removing that test-only bypass is a genuine hardening win and worth calling out in the commit message.
  • Test coverage in measurement.rs is good, including the enumeration that asserts each redacted field appears exactly once.

Four commits, cleanly scoped (policy -> runtime continuity -> migtd-hash -> mapping finalization). I would keep them split; they bisect well.

Findings

# Severity Location Issue
1 High spdm_vdm.rs:28,32 vs spdm_req.rs:351,945 Declared element_count no longer matches the number of encoded elements
2 High spdm_req.rs, spdm_rsp.rs Undeclared wire break; PR description says the opposite
3 Medium mig_policy.rs:353 SERVTD_EXT is never cross-checked against the peer's own quote
4 Medium policy.rs, mig_policy.rs:347 Peer-supplied mapping authorizes the peer; anchored only by root CA + leaf Subject Name
5 Medium removed verify_peer_init_tdinfo_against_owner policySvn monotonicity init -> current no longer enforced
6 Low servtd_collateral.rs check_engine_not_older Now dead code

Findings 1-3 are inline. The rest:

4. The source's own mapping authorizes the source

verify_init_servtd_svn_order(&verified_policy_src.servtd_tcb_mapping, ...) resolves the source's SVNs using the peer-supplied mapping, which is deliberately excluded from RTMR2. Its only anchor is verify_signature(issuer_chain) plus validate_peer_cert_chain, which requires an identical root CA and an identical leaf Subject Name, with intermediates intentionally uncompared and no EKU check.

So any certificate issued under that root with a matching Subject Name can mint a mapping that assigns a high SVN to a revoked or ancient tdinfo_hash. The rationale is legitimate -- an older destination genuinely cannot predict future source releases -- and the PR already lists EKU/trust-anchor hardening and signer revocation as follow-up. But this is the load-bearing trust assumption of the entire change, and I think it should be stated explicitly in doc/policy_v2.md rather than appearing only under "Future changes". A reader of the design doc should not have to infer it.

Worth considering as additional defence: when the destination's local mapping already contains the peer's tdinfo_hash, require the resolved SVN to agree with the local one. That costs nothing in the common case and confines the peer-supplied mapping to genuinely unknown (newer) hashes.

5. policySvn monotonicity between init and current

The removed verify_peer_init_tdinfo_against_owner / _against_suppl_data enforced init_policy_svn <= peer_policy_svn and MROWNER equality on the source path. policySvn is now covered only transitively, via MROWNERCONFIG -> tdinfo_hash -> isvsvn. Since cumulative mappings intentionally allow multiple hashes at the same isvsvn, an init -> current transition that lowers policySvn while keeping the same isvsvn is now accepted.

I suspect this is intended and acceptable, but it is a real loosening relative to main and is not mentioned in the PR description. Could you confirm, and note it in the doc if so?

6. check_engine_not_older is dead code

It is referenced only by its own unit test. Because it is pub in a library crate there is no dead_code warning, so it will sit there indefinitely advertising a check that is no longer performed. That is a trap for the next reader of a security-critical module -- please remove it, or wire it back in if the check is still wanted.

Minor / hardening

  • canonical_value_bytes serializes non-finite floats (e.g. 1e400) as null, so two distinct inputs can canonicalize identically. Not exploitable today, because every numeric policy field is typed u16/u32 and rejects those at deserialization -- but rejecting non-integer or non-finite numbers during canonicalization would close it permanently and cheaply.
  • parse_policy_data accepts both a bare policyData object and a {"policyData": ...} wrapper, so two distinct byte strings canonicalize identically. Harmless in practice (the peer path via RawPolicyData always supplies the wrapper), but a measurement primitive is a place where I would rather have exactly one accepted representation.
  • No tests accompany the SPDM framing changes, which is where findings 1 and 2 live.

Nothing here blocks the design. I would like to see 1-3 addressed before merge, and 4-5 at least documented.

Comment thread src/migtd/src/spdm/spdm_req.rs
Comment thread src/migtd/src/mig_policy.rs Outdated
@haitaohuang

Copy link
Copy Markdown
Contributor Author

@mingweishih thanks for the review. For findings 1-3, see my reply inline.

Recommended in finding 4 - " when the destination's local mapping already contains the peer's tdinfo_hash, require the resolved SVN to agree with the local one. That costs nothing in the common case and confines the peer-supplied mapping to genuinely unknown (newer) hashes."
will add later as enhancement

finding 5 - intentional

finding 6 - will remove dead code and tests

Use SHA-384 over the complete unmasked TDINFO as the Policy v2 mapping key.
Canonicalize policyData once with only the circular mapping removed so
runtime verification and offline tooling extend identical RTMR2 bytes.

Verify the signed mapping with the RTMR1-bound policy issuer chain, remove
the separate mapping chain and obsolete mapping identity fields, and ignore
the legacy outer policy signature as required by the proposal.

Resolve the initial SVN through the source's authenticated JSON mapping
and take its current SVN from the authenticated quote or TDREPORT
evaluation. This allows an older destination to accept a newer source
release without predicting its hash. Fail closed on missing authenticated
SVN evidence and prevent the SERVTD_EXT current hash from overriding it.

Signed-off-by: Haitao Huang <haitaohuang@microsoft.com>
Assisted-by: GitHub Copilot CLI:GPT-5.6 Sol
Assisted-by: GitHub Copilot CLI:gpt-6-astra
Carry authenticated SERVTD_EXT continuity evidence through migration and
rebinding. Bind the peer policy issuer chain to attested RTMR1, reject
lookup misses or SVN rollback, and fail closed when SERVTD_ATTR masking
makes the endorsed unmasked hashes inapplicable.

Match policy-v2 migration and rebinding request headers to their four
encoded elements while retaining the five-element policy-v1 migration
format.

Signed-off-by: Haitao Huang <haitaohuang@microsoft.com>
Assisted-by: GitHub Copilot CLI:GPT-5.6 Sol
Assisted-by: GitHub Copilot CLI:gpt-6-astra
Retain authority-maintained hash history when adding a release, allow
multiple hashes at one SVN, and reject conflicting duplicate assignments.
Emit deterministic mapping bytes and validate signed mappings before use.

Update the policy-v2 guide and mapping-update example for the cumulative
workflow, including the required --mapping-isvsvn argument. Document
mapping signer authority, trust assumptions, and the distinction between
release-SVN continuity and independent policy-SVN ordering.

Signed-off-by: Haitao Huang <haitaohuang@microsoft.com>
Assisted-by: GitHub Copilot CLI:GPT-5.6 Sol
Assisted-by: GitHub Copilot CLI:gpt-6-astra
Consume the signed identity used to build the measured release instead of re-signing it after recording tdinfo_hash. Reject any non-mapping policy change before replacing the final policy outputs, including identity, issuer-chain, and platform collateral changes.

Document preparation before measurement, immutable release inputs, and a final image hash comparison against the recorded endorsement.

Assisted-by: GitHub Copilot CLI:gpt-6-astra
Signed-off-by: Haitao Huang <haitaohuang@microsoft.com>
Remove check_engine_not_older and its dedicated test because runtime continuity uses the MigTD helper instead. Preserve the equal-SVN assertion in the live continuity tests alongside the existing upgrade, downgrade, and lookup-failure coverage.

Signed-off-by: Haitao Huang <haitaohuang@microsoft.com>
Assisted-by: GitHub Copilot CLI:gpt-6-astra
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants