Skip to content

DCAP attestation: Add GCP provenence check to establish whether the associated PPID is endorsed by Google - #54

Open
ameba23 wants to merge 23 commits into
mainfrom
peg/gcp-provenance
Open

DCAP attestation: Add GCP provenence check to establish whether the associated PPID is endorsed by Google#54
ameba23 wants to merge 23 commits into
mainfrom
peg/gcp-provenance

Conversation

@ameba23

@ameba23 ameba23 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

When verifying an attestation which claims to be of type AttestationType::GcpTdx this PR adds an additional check as to whether the PPID from the PCK certificate included in the attestation is present in GCP's public bucket, which indicates that the PPID belongs to them. This is essentially a 'proof-of-cloud' check specially for GCP.

This is based on Google's own provenance checker tool written in Go: https://github.com/google/go-tdx-guest/blob/main/tools/gceprovenance/main.go

In order to match the Go implementation we:

  • Validate that PPIDs must be 16 bytes
  • Have a 30s timeout on the complete fetch
  • Check that the response has 200 status
  • Check that the response is valid JSON

Unlike the Go implementation we additionally:

  • To avoid unnecessary network calls on subsequent verifications, known GCP PPIDs are cached for up to one week. We might want to make this cache time shorter.
  • Validate that the response has fields zone and timestamp.
  • Cap the response size at 16kb.

Note: The Go implementation offers an additional instance-verification check as well as the provenance check. This checks the MR_OWNER value against instance metadata. This is outside of the scope of this PR and not implemented.

Note: No checks are made on the timestamp or zone details in the response, other than making sure those fields are present. This is because the Go implementation also does not check those values. Such checks could be added later in a follow-up.

See relevant documentation from Google:

TODO:

  • Validity window for cached PPIDs (store retrieval timestamp in cache)
  • Fix measurement policy to explicitly match GCP attestation type - otherwise this check is useless
  • Review against the Go implementation: https://github.com/google/go-tdx-guest/tree/main/tools/provenance
  • Tests - do we check PPID extraction with our existing DCAP test assets?

@ameba23
ameba23 marked this pull request as draft June 12, 2026 07:15
@ameba23
ameba23 marked this pull request as ready for review July 6, 2026 09:56
* main:
  Bump reqwest to 0.13.4
  chore(deps): bump openssl from 0.10.79 to 0.10.80
expected_input_data: [u8; 64],
pccs: Option<Pccs>,
) -> Result<MultiMeasurements, DcapVerificationError> {
) -> Result<(MultiMeasurements, Quote), DcapVerificationError> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

To avoid parsing the quote a second time to extract the PPID after verification, the verifier function now returns the parsed quote.

@@ -0,0 +1,70 @@
//! On GCP check MRTD values map to Google endorsed firmware

@ameba23 ameba23 Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This file is mostly unchanged from main - i just refactored it into a separate file to avoid having both provenance and firmware stuff together in one file.

/// OS image
pub measurement_id: String,
/// The attestation type this record accepts
pub attestation_type: AttestationType,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

To enforce the GCP provenance check, attestation policies need to explictly state the expected attestation type

Comment on lines +14 to +15
const GCP_PROVENANCE_REGISTRY_URL: &str =
"https://storage.googleapis.com/confidential-host-registry";

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.

should this be always fixed or should we also make it optionally set by an env var and if not provided it defaults to this URL instead?
This could also serve for future path in case PPID is deprecated or if the verifier would like to pull the registry from a different source.

Comment on lines +75 to +100
{
let known_gcp_ppids = self
.known_gcp_ppids
.read()
.map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))?;
if let Some(stored_at) = known_gcp_ppids.get(&ppid) &&
is_cache_entry_fresh(*stored_at, now)
{
return Ok(());
}
}

// Re-check under the write lock in case another thread refreshed the
// entry while we were waiting, and drop stale entries so we refetch.
{
let mut known_gcp_ppids = self
.known_gcp_ppids
.write()
.map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))?;
if let Some(stored_at) = known_gcp_ppids.get(&ppid) {
if is_cache_entry_fresh(*stored_at, now) {
return Ok(());
}
known_gcp_ppids.remove(&ppid);
}
}

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 redundant and can be simplified on its own

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah i think you are right, we don't need the extra check

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

i just tried to replace this with this, which is much simpler:

        {
            let known_gcp_ppids = self
                .known_gcp_ppids
                .read()
                .map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))?;
            if known_gcp_ppids
                .get(&ppid)
                .is_some_and(|stored_at| is_cache_entry_fresh(*stored_at, now))
            {
                return Ok(());
            }
        }

This works, but we lose the code path which removes stale entries. Which gives correct behavior but means the cache can get bigger and bigger over time.

Comment on lines +211 to +212
#[error("blocking task join: {0}")]
TaskJoin(String),

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.

Nit: semantically doesn't seem to be a GcpProvenanceError type but rather a threading or async kind of error type, or did I misunderstand something here ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No, semantically its not. This would happen if the task handling the provenance check panics. Which if this is implemented correctly should be highly unlikely to happen but we have to handle this case anyway. I would argue it make sense to categorize it as this because it is a possible bad outcome of running the provenance check.

@MoeMahhouk

Copy link
Copy Markdown
Member

Small suggestion:
This provenance check is supposed to be a new addition for the verification path only, probably it would be good to gate it behind a flag instead of making it enforced for every gcp dcap attestation verification process. wdyt?
This way, it would leave it up to the verifier to choose if they want to check attestation & provenance or only attestation would suffice for their workload?

@ameba23

ameba23 commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Small suggestion: This provenance check is supposed to be a new addition for the verification path only, probably it would be good to gate it behind a flag instead of making it enforced for every gcp dcap attestation verification process. wdyt? This way, it would leave it up to the verifier to choose if they want to check attestation & provenance or only attestation would suffice for their workload?

If i understand you right, you mean we should handle the case that the verifier doesn't care whether or not its a GCP attestation - any DCAP attestation will do. Currently you can get this by specifying 'DcapTdx' as the attestation type in the policy. However, if the server claims their attestation type is GcpTdx in the PlatformMetadata, we still do this check and bail if it fails, even though the verifier doesn't require it. So maybe we should only do the check if the verifier enforces attestation type to be GcpTdx.

I see the logic here, but i'd be wary of skipping the check in that case, because if the server indicates that they are running on GCP but is not, i don't think we should treat that as a valid attestation. In the happy path, the non-GCP server correctly submits platform metadata as DcapTdx and the provenance check never happens.

If your concern is more just that we are starting to bloat the whole process with GCP-specific logic, then yes i can see an argument for putting it behind a feature flag and not compiling it on builds where we don't care about GCP.

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.

2 participants