Two residues from #74 and #75, tracked so they do not live only in LIMITATIONS.md.
1. Chained verification cannot succeed on some Azure hosts
ca2a_verify/tpm_roots.py ships AZURE_VTPM_ROOT_2023_PEM as an opt-in anchor. Measured on hardware 2026-08-01: Azure Trusted Launch runs two vTPM CA hierarchies concurrently, and which one a VM presents is not selectable.
|
D2s_v5, eastus |
D2s_v7, eastus2 |
| AK certificate |
1596 bytes |
994 bytes |
| Issuer |
Azure Cloud Virtual TPM CA - 11 |
Global Virtual TPM CA - 03 |
| AIA extension |
present |
absent entirely |
| Chain to the shipped root |
yes |
no |
On the second, there is nothing to walk and the intermediates are not in NV either, so verify_tpm_report fails closed with "AK chain root is not among the supplied trusted TPM roots". That is the correct outcome, because key provenance genuinely cannot be established there.
What this means for cA2A specifically: verify_offer reaches assurance="hardware" only where the peer's host happens to present the chainable hierarchy. A caller cannot know in advance whether a peer can meet that bar. The sealed-channel property that depends on it is therefore host-dependent, which is a stronger caveat than the one LIMITATIONS.md carried before #75.
Blocked on: obtaining the Global Virtual TPM CA - 03 root from a citable Microsoft source. That is a sourcing problem, not a code problem, and it is tracked on the cmcp side in agentrust-io/cmcp#453. When that root exists, pin both here.
Also still unproven: a collect-then-verify pass on a host that does present the chainable hierarchy. The 2026-08-01 run drew the no-AIA variant, so chained verification has never completed against a real vendor root in cA2A. Worth deliberately provisioning until a D2s_v5-class host appears.
2. parse_tpmt_signature is a duplicate
ca2a_verify/tpm.py carries its own TPMT_SIGNATURE parser because agent-manifest does not model that wire format, and its verify_tpm_quote takes a bare signature. cmcp carries the same parser for the same reason. This was a deliberate call when #74 landed (ship now, consolidate later) rather than an oversight.
Tracked upstream as agentrust-io/agent-manifest#255. When it lands: bump the agent-manifest floor, delete ParsedSignature and parse_tpmt_signature from ca2a_verify/tpm.py, and import instead. The tests in test_tpm_attest.py that cover the parser directly can then move upstream too.
What is already correct and needs nothing
Worth recording so this issue is not misread as the TPM path being broken. On real hardware the collector produced a genuine platform-AK quote, parse_tpmt_signature unwrapped a real TPMT_SIGNATURE (RSASSA/SHA-256), the bare signature verified against the shipped key, a tampered attest blob was rejected, and the quote's extraData equalled the derived key-and-nonce binding. Collector and verifier also ran in one process, which retired an earlier caveat. Only the chain step is host-dependent.
Two residues from #74 and #75, tracked so they do not live only in
LIMITATIONS.md.1. Chained verification cannot succeed on some Azure hosts
ca2a_verify/tpm_roots.pyshipsAZURE_VTPM_ROOT_2023_PEMas an opt-in anchor. Measured on hardware 2026-08-01: Azure Trusted Launch runs two vTPM CA hierarchies concurrently, and which one a VM presents is not selectable.D2s_v5, eastusD2s_v7, eastus2Azure Cloud Virtual TPM CA - 11Global Virtual TPM CA - 03On the second, there is nothing to walk and the intermediates are not in NV either, so
verify_tpm_reportfails closed with "AK chain root is not among the supplied trusted TPM roots". That is the correct outcome, because key provenance genuinely cannot be established there.What this means for cA2A specifically:
verify_offerreachesassurance="hardware"only where the peer's host happens to present the chainable hierarchy. A caller cannot know in advance whether a peer can meet that bar. The sealed-channel property that depends on it is therefore host-dependent, which is a stronger caveat than the oneLIMITATIONS.mdcarried before #75.Blocked on: obtaining the
Global Virtual TPM CA - 03root from a citable Microsoft source. That is a sourcing problem, not a code problem, and it is tracked on the cmcp side in agentrust-io/cmcp#453. When that root exists, pin both here.Also still unproven: a collect-then-verify pass on a host that does present the chainable hierarchy. The 2026-08-01 run drew the no-AIA variant, so chained verification has never completed against a real vendor root in cA2A. Worth deliberately provisioning until a
D2s_v5-class host appears.2.
parse_tpmt_signatureis a duplicateca2a_verify/tpm.pycarries its ownTPMT_SIGNATUREparser because agent-manifest does not model that wire format, and itsverify_tpm_quotetakes a bare signature. cmcp carries the same parser for the same reason. This was a deliberate call when #74 landed (ship now, consolidate later) rather than an oversight.Tracked upstream as agentrust-io/agent-manifest#255. When it lands: bump the
agent-manifestfloor, deleteParsedSignatureandparse_tpmt_signaturefromca2a_verify/tpm.py, and import instead. The tests intest_tpm_attest.pythat cover the parser directly can then move upstream too.What is already correct and needs nothing
Worth recording so this issue is not misread as the TPM path being broken. On real hardware the collector produced a genuine platform-AK quote,
parse_tpmt_signatureunwrapped a realTPMT_SIGNATURE(RSASSA/SHA-256), the bare signature verified against the shipped key, a tampered attest blob was rejected, and the quote'sextraDataequalled the derived key-and-nonce binding. Collector and verifier also ran in one process, which retired an earlier caveat. Only the chain step is host-dependent.