feat: use complete TDINFO hash for TCB mappings - #1032
haitaohuang wants to merge 5 commits into
Conversation
e3dd4df to
ec626be
Compare
|
@haitaohuang , is any Microsoft people can review and approve as well? |
There was a problem hiding this comment.
🟡 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, butmigtd-hashis 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:126fail 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-70still instructs users to signpolicyDataand 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.
aa9fc2d to
bd32692
Compare
|
bd32692 to
ae57d92
Compare
mingweishih
left a comment
There was a problem hiding this comment.
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 onserde_json::to_vecordering. That is the right call, becauseserde_json/preserve_orderis 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 becauseunbounded_depthis off, so serde_json's 128-level limit applies. - Runtime and
migtd-hashshare one implementation ofextract_canonical_policy_data_bytes(tools/migtd-hash/src/lib.rsvssrc/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_denyfails closed whenmigtd_tcb_statusisNone, so atdinfo_hashabsent from the signed mapping is denied rather than defaulted.TdTcbMapping::validate()rejects wrong-length digests and rejects the same hash mapped to differingisvsvn, which closes the first-match ambiguity inget_engine_svn_by_measurements.get_tcb_level_by_svnfloor-matching returnsNonebelow all levels, i.e. denied.verify_init_servtd_svn_orderis called unconditionally, whereas the code it replaces was gated bycfg(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.rsis 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_bytesserializes non-finite floats (e.g.1e400) asnull, so two distinct inputs can canonicalize identically. Not exploitable today, because every numeric policy field is typedu16/u32and rejects those at deserialization -- but rejecting non-integer or non-finite numbers during canonicalization would close it permanently and cheaply.parse_policy_dataaccepts both a barepolicyDataobject and a{"policyData": ...}wrapper, so two distinct byte strings canonicalize identically. Harmless in practice (the peer path viaRawPolicyDataalways 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.
|
@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." 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
ae57d92 to
708d640
Compare
Summary
Implement the one-hash TCB mapping design proposed in #908:
SHA384(TDINFO)over the complete unmasked TDINFO as the mapping key;policyDatawith onlyservtdTcbMappingredacted;SERVTD_EXTrather than host-supplied Init_TDINFO;SERVTD_ATTR;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
Future changes
Potential follow-up work, intentionally excluded from this focused series: