Skip to content

fix(attestation): include Azure vTPM AK intermediates#49

Open
samlaf wants to merge 10 commits into
flashbots:mainfrom
SeismicSystems:fix/azure-vtpm-aia-intermediates-clean
Open

fix(attestation): include Azure vTPM AK intermediates#49
samlaf wants to merge 10 commits into
flashbots:mainfrom
SeismicSystems:fix/azure-vtpm-aia-intermediates-clean

Conversation

@samlaf

@samlaf samlaf commented Jun 5, 2026

Copy link
Copy Markdown

This fixes Azure TDX/vTPM attestation verification failures caused by newer Azure vTPM AK certificate chains that are not covered by the currently bundled Global Virtual TPM CA - 03 intermediate.

On a current Azure TDX CVM, the AK leaf certificate chain can look like:

AK leaf
  issuer: Azure Cloud Virtual TPM CA - 24

Azure Cloud Virtual TPM CA - 24
  issuer: Azure Cloud Virtual TPM CA 2025

Azure Cloud Virtual TPM CA 2025
  issuer: Azure Virtual TPM Root Certificate Authority 2023

The TPM NV AK cert index on this VM contained only:

leaf DER || zero padding

so parsing additional intermediates from the TPM AK cert payload is not sufficient for this chain.

This PR changes Azure attestation generation to fetch the AK issuer chain from the leaf certificate’s AIA CA Issuers URLs and serialize those intermediates into the attestation evidence. Verification remains offline: the verifier treats these intermediates as untrusted chain material and still pins the Azure vTPM roots.

Microsoft guidance indicates that AIA should be used to discover Azure vTPM intermediate CAs, since public docs can lag behind production intermediate rotation:

https://learn.microsoft.com/en-us/answers/questions/5897616/download-intermediate-ca-cert-for-azure-cloud-virt

Changes

  • Adds optional evidence field:

    ak_intermediate_certificates_pem: Vec<String>
  • During Azure attestation generation:

    • reads the AK leaf certificate from the vTPM;
    • strips TPM NV zero padding;
    • walks AIA CA Issuers URLs up to a bounded depth;
    • serializes fetched intermediate certs into the evidence.
  • During verification:

    • deserializes evidence-supplied intermediates;
    • bounds the number of supplied intermediates;
    • combines them with the legacy bundled intermediate for backwards compatibility;
    • verifies the AK leaf against pinned Azure vTPM roots.
  • Keeps verification network-free/deterministic.

  • Keeps #[serde(default)] on the new field so old evidence still deserializes.

  • Adds offline fixture tests for the observed Azure chain:

    • Azure Cloud Virtual TPM CA - 24
    • Azure Cloud Virtual TPM CA 2025

Security notes

The fetched intermediates are not trusted anchors. They are serialized as untrusted evidence only. Verification still requires the AK certificate chain to terminate at a pinned Azure vTPM root.

The number of evidence-supplied intermediates is capped to avoid unbounded peer-controlled chain material.

Behavior change

Azure attestation generation now makes outbound HTTP(S) requests to Microsoft PKI/AIA endpoints in order to fetch issuer certificates.

Azure attestation verification does not make network requests.

Validation

Tested with this code on an Azure Standard_DC4eds_v6 TDX box on westus3. Before this change, verification failed with:

WebPKI: UnknownIssuer

After this change:

Generating Azure TDX + vTPM attestation...
Generated azure-tdx evidence: 22148 bytes
Verifying Azure TDX + vTPM attestation...
Verification succeeded.

The generated evidence includes two serialized AK intermediates:

Azure Cloud Virtual TPM CA - 24
Azure Cloud Virtual TPM CA 2025

This subsumes #48.

PR #48 improves handling for cases where the TPM AK cert payload contains concatenated DER intermediates. However, on the Azure TDX VM tested here, the TPM AK cert NV index contained only:

leaf DER || zero padding

and no intermediate certificates. Therefore #48 does not fix the observed UnknownIssuer failure for this chain.

This PR handles that case by following the AIA issuer URLs from the AK leaf certificate and including the resulting issuer chain in the evidence.

Fetch Azure vTPM AK issuer certificates from the leaf certificate's AIA CA Issuers URLs during attestation generation and serialize them into the Azure evidence document.

During verification, treat evidence-supplied intermediates as untrusted chain material, combine them with the legacy bundled intermediate for backwards compatibility, and continue pinning Azure vTPM roots. Bound the number of serialized intermediates during deserialization and keep stripping TPM NV zero padding before WebPKI verification.

Add offline AK-chain fixtures for the observed Azure Cloud Virtual TPM CA - 24 -> Azure Cloud Virtual TPM CA 2025 chain.

@ameba23 ameba23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for the PR! We have let support for Azure slip, because we currently have no production Azure instances. So wonderful to have this fix contributed.

Very good idea to do the intermediary certificate fetching on the 'attester' side rather than the verifier side. One thing we could consider for a followup would be to cache these to avoid re-fetching on subsequent attestation generation.

Great to have the test assets with observed intermediaries. I think it could be useful to have a complete observed attestation payload to test against. We could spin up an Azure instance and get one if you don't want to add this yourself.

We use stable rust but nightly for formatting / clippy, which is why CI fails. This is unconventional and not documented (sorry). Could you please run cargo +nightly fmt && cargo +nightly clippy. Thanks.

@ameba23 ameba23 requested a review from MoeMahhouk June 8, 2026 10:51
pub(crate) fn fetch_ak_intermediates_from_aia(
ak_cert: &X509Certificate<'_>,
) -> Result<Vec<Vec<u8>>, MaaError> {
const MAX_AIA_DEPTH: usize = 6;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems to be too arbitrary. Why hardcode the depth? why not fetch all intermediates to verify the full chain? what happens if there were slightly more than 6 for example? would the full chain verification up to the Azure Root CA anchor still succeed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch! Refactored the fetching logic quite a bit in bd465dd, in part to fetch from all possible URLs, not only the first one; let me know what you think.

As for your question, there was a big inconsistency. I combined MAX_AIA_DEPTH with the more important MAX_EVIDENCE_AK_INTERMEDIATE_CERTIFICATES which is used on the verifier path. The main idea is that we need to protect from DDoS attacks on the verifier side, so we need some bound. 6 is indeed very arbitrary... but I somehow doubt Microsoft would have chains greater than 2/3.... pure hunch though I really have no idea. Happy to increase if you think we should.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Agree that there has to be a limit to prevent attacks, but i'm not sure what it should be. 6 or 8 sounds like plenty to me.

Comment thread crates/attestation/src/azure/ak_certificate.rs Outdated
Comment thread crates/attestation/src/azure/ak_certificate.rs
@MoeMahhouk

Copy link
Copy Markdown
Member

@samlaf , thank you for surfacing this new change from Azure vTPM attestation process side as well as for your contribution to solve the issue.
I skimmed the PR changes quickly and left some comments to review.
Also, I have an extra question:
why is it delegated to the attester to fetch the intermediate certificates along side the vTPM AK leaf certificate?
If I understand this link you shared in the code comments, Azure support recommendation sounded more like it is on the verifier to fetch those intermediate certificate by using the AIA url provided within the AK leaf certificate received during the attestation by the attester.
So I would think that the flow from the attester side stays the same while the verifier should parse the AK leaf certificate to fetch the AIA url and fetch all intermediate certificates used to verify the full certificate chain up to the Azure vTPM anchor CA certificate. Or did I oversee something?

@samlaf samlaf force-pushed the fix/azure-vtpm-aia-intermediates-clean branch from 8b1bc40 to 94a74a3 Compare June 8, 2026 15:29
samlaf added 7 commits June 8, 2026 23:30
fix output dir for fixtures

commit fixtures

update test to use new yaml fixtures

fix fixture generating code (serde_saphyr bug?)

actually fix serializer

make fixture match new serialization format
make from maaerror box

fix clippy: only box MaaError::AiaFetch
Consolidate AK intermediate bounds into a single MAX_EVIDENCE_AK_INTERMEDIATE_CERTIFICATES limit shared by generation and verification, so generated evidence cannot exceed what the verifier accepts.

Fail closed when the AK issuer chain is incomplete or exceeds that limit instead of silently returning a partial chain. When following AIA CA Issuers URLs, try alternate URLs sequentially as fallbacks if earlier URLs fail, and trim fetched DER to the parsed certificate before serializing it as evidence.

Box the large ureq AIA fetch error inside MaaError to keep the error enum size small enough for clippy.
Add local HTTP server tests for AK issuer fetching to verify that AIA caIssuers URLs are tried sequentially when earlier URLs fail, and that explicit PEM responses are accepted as a lenient fallback to DER.

Keep the tests deterministic by using a local server instead of depending on Azure or Microsoft CDN availability.
@samlaf samlaf force-pushed the fix/azure-vtpm-aia-intermediates-clean branch from 94a74a3 to 01e60d4 Compare June 8, 2026 15:30
@samlaf

samlaf commented Jun 8, 2026

Copy link
Copy Markdown
Author

Thanks for the quick feedback guys! Always fun to work with responsive people. :)

Sorry had pushed some unsigned commits from the TDX box... so had to force-push to squash them. Will address comments now.

Great to have the test assets with observed intermediaries. I think it could be useful to have a complete observed attestation payload to test against. We could spin up an Azure instance and get one if you don't want to add this yourself.

@ameba23 refactored the tests to follow your previous yaml file convention. Let me know what you think! 74a7881

So I would think that the flow from the attester side stays the same while the verifier should parse the AK leaf certificate to fetch the AIA url and fetch all intermediate certificates used to verify the full certificate chain up to the Azure vTPM anchor CA certificate. Or did I oversee something?

@MoeMahhouk good question... both options are possible. I went with this option because it felt better in a TEE world to have verification be deterministic, not require a network connection, and be totally self-contained. But if you think otherwise we could support verifier fallbacking to fetching from network. Depends how flexible (but also bug-prone!) we want this library to be I guess. Added a comment on create_azure_attestation in 2ef3e85, let me know what you think.

Comment on lines +644 to +650
let mut serializer_options = serde_saphyr::SerializerOptions::default();
// With compact list indentation enabled, serde_saphyr can emit an
// indentless empty sequence after a nested block sequence, e.g.
// `event_log:\n[]`, which its parser rejects. Disable compact list
// indentation for fixture output so nested/empty sequences are always
// indented under their mapping keys.
serializer_options.compact_list_indent = false;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Seems like this bug was fixed: bourumir-wyngs/serde-saphyr#126
Prob worth bumping your serde-saphyr dep?

@ameba23

ameba23 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

@ameba23 refactored the tests to follow your previous yaml file convention. Let me know what you think! 74a7881

Great! Yes nice to see a full verification working, and thanks for also adding the code to generate more test assets later should things change.

@MoeMahhouk good question... both options are possible. I went with this option because it felt better in a TEE world to have verification be deterministic, not require a network connection, and be totally self-contained. But if you think otherwise we could support verifier fallbacking to fetching from network. Depends how flexible (but also bug-prone!) we want this library to be I guess. Added a comment on create_azure_attestation in 2ef3e85, let me know what you think.

I think it depends on the use-case which is better, and if we want the attestation crate to be a general purpose library we could consider adding both. But for the attested-tls crate/protocol in this repo, there are some big advantages to having the fetching done on the attester side:

  • The attestation verification happens in rustls's ServerCertVerifier trait implementation which is has a synchronous verifier function called during the handshake which is not designed to do network calls. It would be possible to do the fetching there but undesirable, especially for projects with async runtimes as this will block. We have already jumped through a lot of hoops to avoid needing to fetch collateral for DCAP during verification.
  • The attestation is bound to a TLS certificate which is verified potentially several times by different clients. Including pre-fetched intemediaries would mean a single fetch regardless of how many times it is verified.

On the other hand, @MoeMahhouk 's concern is that some of our CVMs run in constrained network environments where the endpoints for fetching intermediaries would have to be explicitly allow-listed, which increases the attack surface and could create problems if Microsoft were to change the hostname providing them.

@ameba23 ameba23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🥇 Approving as i think this is a much needed and well-crafted fix.

But will not merge until @MoeMahhouk is happy with the idea of doing attester-side intemediary fetching, due to the potential issues with flashbox.

Would be good to also get a thumbs up from @0x416e746f6e

}

fn fetch_certificate_der(url: &str) -> Result<Vec<u8>, MaaError> {
if !(url.starts_with("http://") || url.starts_with("https://")) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not sure if we should have extra checks here on the hostname/IP. A malicious vTPM could cause a network call to arbitrary host here. But i can't see how they would turn that into a meaningful attack.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Were you thinking a blacklist (eg. disallow localhost) or a whitelist? Something like this perhaps?

  fn is_allowed_azure_aia_host(host: &str) -> bool {                                                                                                                                                                                                       
      let host = host.trim_end_matches('.').to_ascii_lowercase();                                                                                                                                                                                          
                                                                                                                                                                                                                                                           
      host == "www.microsoft.com"                                                                                                                                                                                                                          
          || host == "crl.microsoft.com"                                                                                                                                                                                                                   
          || host == "primary-cdn.pki.core.windows.net"                                                                                                                                                                                                    
          || host == "secondary-cdn.pki.core.windows.net"                                                                                                                                                                                                  
          || host.ends_with(".pki.core.windows.net")                                                                                                                                                                                                       
  }                                                                                                                                                                                                                                                        

But then again I'm not sure this is even true and whether azure won't introduce other names...

}

issuer_urls = fetched_issuer.ca_issuers_urls;
intermediates.push(fetched_issuer.der);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it might be better to remove the check: if fetched_issuer.is_self_signed { (above) and instead, do:

Suggested change
intermediates.push(fetched_issuer.der);
intermediates.push(fetched_issuer.der);
if verify_ak_cert_with_azure_roots(ak_cert_der, &intermediates, now_secs).is_ok() {
return Ok(intermediates);
}

This will avoid an unneeded fetch for the root cert which we already have, and make us sure right away that we have a complete chain which verifies against the known root.

where
D: serde::Deserializer<'de>,
{
let certificates = Vec::<String>::deserialize(deserializer)?;

@ameba23 ameba23 Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe worth adding a maximum string length check here to avoid attempting to parse giant blobs as pem / der. But actually it would be better to cap the size of the whole payload because attempting to de-serialize in prepare_azure_attestation. Which is kind of needed anyway unrelated to this PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agree that this should be done in a follow-up small self-contained PR. Created #50

@MoeMahhouk

Copy link
Copy Markdown
Member

🥇 Approving as i think this is a much needed and well-crafted fix.

But will not merge until @MoeMahhouk is happy with the idea of doing attester-side intemediary fetching, due to the potential issues with flashbox.

Would be good to also get a thumbs up from @0x416e746f6e

@ameba23 , no blockers from my side. We can merge to unblock if this is needed elsewhere.
For our different use-cases with more constrained workloads in CVMs like Flashbox, we can either allow-list such endpoints or consider refactoring this code to put it behind an optional flag and let the use-case decide wether the attester should fetch intermediate certificates or delegate it to the issuer.

Verify the AK certificate against pinned Azure roots after each AIA-fetched issuer is added, and stop once the fetched chain is complete. This avoids fetching the root certificate in normal Azure vTPM chains.

Keep the bundled Azure intermediate as a legacy verification-only fallback for older evidence, but exclude it while generating new AIA-based evidence so the serialized intermediates are self-contained.

Also convert Azure wall-clock lookup failures into MaaError instead of panicking.
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.

3 participants