Skip to content

docs(tee): record the NV extend hardware run, and correct the Azure PKI claim - #452

Merged
imran-siddique merged 1 commit into
mainfrom
docs/tpm-hardware-findings
Aug 1, 2026
Merged

imran-siddique merged 1 commit into
mainfrom
docs/tpm-hardware-findings

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Ran the TPM path on a real Azure Trusted Launch vTPM (Standard_D2s_v7, eastus2, 2026-08-01), specifically to validate the calls #451 shipped without ever executing against a TPM. Docs and comments only.

1. The NV extend index from #451 works on hardware

Every TPM call in cmcp_runtime.tee.measurement had been written against the documented tpm2-pytss API and exercised only against a fake. All of them are now confirmed:

Check Result
nv_define_space(None, nv_public, auth_handle=ESYS_TR.OWNER) index created
TPMA_NV.parse(...) | (TPM2_NT.EXTEND << 4) TPM_NT = 4 read back from the public area
tpm2_getcap handles-nv-index 0x01500432, size 32
nv_extend(handle, digest, auth_handle=ESYS_TR.OWNER) extended
First call provisions, second reuses provisioned=True then False
Accumulation before2 == after1, value moved
after == H(before || digest) held on both extends
Plain nv_write to the index refused by the TPM

That last row is the load-bearing one: it is what makes an adversary unable to write a chosen clean value, and it is corroborated independently by the TPM_NT field rather than resting on an exception type. The nv_extend keyword-argument form this module uses is also confirmed correct.

Still open for #432: the index value travels as an ordinary NV read that no signature covers, so it remains a local integrity control rather than remote-verifiable evidence. TPM2_NV_Certify is the remaining half.

2. The 2026-07-31 Azure PKI conclusion is wrong

docs/testing/hardware-validation.md currently says Azure "has changed its vTPM PKI", based on finding the AK certificate issued by Azure Cloud Virtual TPM CA - 11 with a walkable AIA chain, superseding an earlier no-AIA observation.

The 2026-08-01 host presented the older form again:

2026-07-31, D2s_v5 eastus 2026-08-01, D2s_v7 eastus2
Certificate size 1596 bytes 994 bytes
Issuer Azure Cloud Virtual TPM CA - 11 Global Virtual TPM CA - 03
AIA extension present, 4 CA Issuers URIs absent entirely
Chain to a pinnable root yes no

tpm2_getcap handles-nv-index returned only 0x01C101D0 plus the test index, so the intermediates are not stored in NV as a fallback either.

This is fleet variance, not a migration, and the distinction changes the design assumption. A migration reading implies AIA can be relied on going forward. What is actually true is that an Azure Trusted Launch host may present either hierarchy, so AIA cannot be relied on, and on the no-AIA host chained verification is impossible: it fails closed with "AK chain root is not among the supplied trusted TPM roots", which is correct, because key provenance genuinely cannot be established there.

Pinning the 2023 root does not make Azure verify everywhere. A deployment must pin the root for the hierarchy its own hosts present, and a fleet spanning both needs both. Noted in the doc and in cmcp_verify/tpm_roots.py.

Worth a second look: #431

#431 was closed on the basis that the runtime ships AK certificate chains with its evidence. It does ship what it can assemble, and the code is correct. But on this fleet there is nothing to assemble, so "ships its chain" does not imply "provenance is verifiable". I have not reopened it, since the code change that closed it stands; flagging it as a judgement call for you.

Note for reviewers

python -m mypy src reports 5 pre-existing errors in src/cmcp_verify/tpm.py (hash_cls() instantiation, dating from #444), in a file this PR does not touch. CI passes, so CI's mypy invocation differs from a bare mypy src; worth a separate look.

🤖 Generated with Claude Code

…KI claim

Ran the TPM path on a real Azure Trusted Launch vTPM (Standard_D2s_v7, eastus2,
2026-08-01). Two outcomes.

The NV extend index from #451 works. Its TPM calls had been written against the
documented tpm2-pytss API without ever touching a TPM, and every one of them is
now exercised: nv_define_space with
TPMA_NV.parse(...) | (TPM2_NT.EXTEND << 4) created a genuine extend index
(TPM_NT = 4 read back from the public area), nv_extend accumulated as
H(old || data) across successive calls, an existing index was reused rather than
redefined, and a plain nv_write to the index was refused by the TPM. That last
check is the one the tamper-evidence argument actually rests on, and it is
confirmed independently by the TPM_NT field rather than only by an exception.

The 2026-07-31 note in hardware-validation.md is wrong in its conclusion. It read
the appearance of Azure Cloud Virtual TPM CA - 11 with a walkable AIA chain as
"Azure has changed its vTPM PKI", superseding an earlier no-AIA observation. The
2026-08-01 host presented the older form again: 994 bytes, issued by
Global Virtual TPM CA - 03, no AIA extension at all, and tpm2_getcap
handles-nv-index confirmed the intermediates are not in NV as a fallback.

So both hierarchies are live concurrently. This is variance, not a migration,
and the difference matters: a migration reading says AIA can be relied on going
forward, while the truth is that a host may present either and chained
verification is impossible on the no-AIA one. Pinning the 2023 root does not
make Azure verify everywhere.

Worth re-reading against #431, which was closed on the basis that the runtime
ships chains with its evidence. It does ship what it can assemble; on this fleet
there is nothing to assemble.

Docs and comments only.

Refs #432, #451, #431
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant