Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 188 additions & 35 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ reqwest = { version = "0.13.4", default-features = false, features = ["rustls"]
rustls = { version = "0.23.37", default-features = false, features = ["brotli"] }
tokio = { version = "1.50.0", features = ["default"] }
tokio-rustls = { version = "0.26.4", default-features = false }
dcap-qvl = { git = "https://github.com/Phala-Network/dcap-qvl.git", rev = "f1dcc65371e941a7b83e3234833d23a1fb232ab1" }
dcap-qvl = "0.5.2"
pccs = { path = "crates/pccs" }
25 changes: 13 additions & 12 deletions crates/attestation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ This crate provides:

## Runtime Requirements

Verification uses the [`pccs`](../pccs) crate for collateral caching and
background refresh. As a result, constructing an `AttestationVerifier` with
PCCS enabled and calling verification APIs is expected to happen from within a
Tokio runtime and might panic if called outside of one.
DCAP attestation generation uses the [`pccs`](../pccs) crate to fetch and bundle
the collateral required to verify the quote. DCAP verification consumes this
bundled collateral, so verifier-side network fetching is not required for the
normal DCAP path.

Note that although some of the verification API methods are synchronous (for
example `verify_attestation_sync`), still their functionality depends on
Tokio-backed background tasks such as PCCS pre-warm and cache refresh.
Constructing generators that fetch DCAP collateral is expected to happen from
within a Tokio runtime because PCCS cache pre-warm and refresh are driven by
Tokio background tasks.

## Feature flags

Expand Down Expand Up @@ -83,11 +83,12 @@ attempted.
Alternatively, an external 'attestation provider service' URL can be provided
which outsources the attestation generation to another process.

When verifying DCAP attestations, the Intel PCS is used to retrieve collateral
unless a PCCS URL is provided via a command line argument. If outdated TCB is
used, the quote will fail to verify. For special cases where outdated TCB
should be allowed, a custom override function can be passed when verifying which
may modify collateral before it is validated against the TCB.
When generating DCAP attestations, Intel PCS is used to retrieve collateral
unless a PCCS URL is configured. The quote and collateral are then serialized
together as the DCAP evidence. If outdated TCB is used, the quote will fail to
verify. For special cases where outdated TCB should be allowed, a custom
override function can be passed when verifying which may modify collateral
before it is validated against the TCB.

## Measurements File

Expand Down
18 changes: 6 additions & 12 deletions crates/attestation/src/azure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ pub enum MaaError {
#[cfg(test)]
mod test_utils {
use base64::{Engine as _, engine::general_purpose::URL_SAFE as BASE64_URL_SAFE};
use dcap_qvl::collateral::CollateralClient;

use super::{AttestationDocument, create_azure_attestation};
use crate::dcap::PCS_URL;
Expand Down Expand Up @@ -662,17 +663,11 @@ mod test_utils {
assert!(intermediate_count > 0, "captured attestation should include AK intermediates");

let quote_bytes = BASE64_URL_SAFE.decode(&attestation_document.tdx_quote_base64).unwrap();
let quote = dcap_qvl::quote::Quote::parse(&quote_bytes).unwrap();
let ca = quote.ca().unwrap();
let fmspc = hex::encode_upper(quote.fmspc().unwrap());
let collateral = dcap_qvl::collateral::get_collateral_for_fmspc(
PCS_URL,
fmspc.clone(),
ca,
false, // TDX, not SGX.
)
.await
.unwrap();
let collateral = CollateralClient::with_default_http(PCS_URL)
.unwrap()
.fetch(&quote_bytes)
.await
.unwrap();

let timestamp =
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
Expand All @@ -698,7 +693,6 @@ mod test_utils {

println!("wrote {}", attestation_path.display());
println!("wrote {}", collateral_path.display());
println!("quote fmspc={fmspc} ca={ca}");
println!("ak_intermediate_certificates_pem entries={intermediate_count}");
}
}
Expand Down
124 changes: 64 additions & 60 deletions crates/attestation/src/dcap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! verification
use dcap_qvl::{
QuoteCollateralV3,
collateral::get_collateral_for_fmspc,
collateral::CollateralClient,
quote::{Quote, Report},
tcb_info::TcbInfo,
};
Expand All @@ -11,7 +11,7 @@ use mock_tdx::generate_mock_tdx_quote;
use pccs::{Pccs, PccsError};
use thiserror::Error;

use crate::{AttestationError, measurements::MultiMeasurements};
use crate::{AttestationError, DcapWithCollateral, measurements::MultiMeasurements};

/// FMSPC with which to override TCB level checks on Azure (not used for GCP
/// or other platforms)
Expand All @@ -20,52 +20,82 @@ const AZURE_BAD_FMSPC: &str = "90C06F000000";
/// For fetching collateral directly from Intel, if no PCCS is specified
pub const PCS_URL: &str = "https://api.trustedservices.intel.com";

/// Generate a TDX quote
pub fn create_dcap_attestation(input_data: [u8; 64]) -> Result<Vec<u8>, AttestationError> {
/// Generate a TDX quote and bundle the collateral needed to verify it.
pub fn create_dcap_attestation(
input_data: [u8; 64],
pccs: Option<&Pccs>,
) -> Result<Vec<u8>, AttestationError> {
let quote = generate_quote(input_data)?;
tracing::info!("Generated TDX quote of {} bytes", quote.len());
Ok(quote)

let fallback_pccs;
let pccs = match pccs {
Some(pccs) => pccs,
None => {
fallback_pccs = Pccs::new_without_prewarm(None);
&fallback_pccs
}
};
let collateral = pccs.get_collateral_for_quote_sync(&quote)?;

Ok(serde_json::to_vec(&DcapWithCollateral { quote, collateral })?)
}

pub fn quote_from_dcap_attestation(input: &[u8]) -> Result<Quote, DcapVerificationError> {
let evidence = parse_dcap_evidence(input)?;
Ok(Quote::parse(&evidence.quote)?)
}

fn parse_dcap_evidence(input: &[u8]) -> Result<DcapWithCollateral, DcapVerificationError> {
serde_json::from_slice(input).map_err(DcapVerificationError::from)
}

/// Verify a DCAP TDX quote, and return the measurement values
#[cfg(not(any(test, feature = "mock")))]
// Keep this async to preserve the public verifier API even though bundled
// collateral makes this implementation synchronous.
#[allow(clippy::unused_async)]
pub async fn verify_dcap_attestation(
input: Vec<u8>,
expected_input_data: [u8; 64],
pccs: Option<Pccs>,
_pccs: Option<Pccs>,
) -> Result<MultiMeasurements, DcapVerificationError> {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
let override_azure_outdated_tcb = false;
verify_dcap_attestation_with_given_timestamp(
input,
let evidence = parse_dcap_evidence(&input)?;
let quote = Quote::parse(&evidence.quote)?;
verify_dcap_attestation_with_collateral_and_timestamp(
evidence.quote,
quote,
expected_input_data,
pccs,
None,
evidence.collateral,
now,
override_azure_outdated_tcb,
)
.await
}

/// Synchronous version - Verify a DCAP TDX quote, and return the
/// measurement values
///
/// This relies on having DCAP collateral already present in the cache
/// This verifies the quote with the collateral bundled in the DCAP
/// evidence, so it does not fetch collateral from the verifier side.
///
/// If possible, prefer the async version
#[cfg(not(any(test, feature = "mock")))]
pub fn verify_dcap_attestation_sync(
input: Vec<u8>,
expected_input_data: [u8; 64],
pccs: Pccs,
_pccs: Pccs,
) -> Result<MultiMeasurements, DcapVerificationError> {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
let override_azure_outdated_tcb = false;
verify_dcap_attestation_with_timestamp_sync(
input,
let evidence = parse_dcap_evidence(&input)?;
let quote = Quote::parse(&evidence.quote)?;
verify_dcap_attestation_with_collateral_and_timestamp(
evidence.quote,
quote,
expected_input_data,
pccs,
None,
evidence.collateral,
now,
override_azure_outdated_tcb,
)
Expand All @@ -74,7 +104,8 @@ pub fn verify_dcap_attestation_sync(
/// Verify a DCAP TDX quote, and return the measurement values, providing a
/// timestamp an optional pre-fetched collateral
///
/// This relies on having DCAP collateral already present in the cache
/// This helper accepts raw quote bytes. If collateral is not provided, it
/// fetches quote collateral via the supplied PCCS client.
///
/// If possible, prefer the async version
pub fn verify_dcap_attestation_with_timestamp_sync(
Expand All @@ -87,13 +118,10 @@ pub fn verify_dcap_attestation_with_timestamp_sync(
) -> Result<MultiMeasurements, DcapVerificationError> {
let quote = Quote::parse(&input)?;

let ca = quote.ca()?;
let fmspc = hex::encode_upper(quote.fmspc()?);

let collateral = if let Some(given_collateral) = collateral {
given_collateral
} else {
pccs.get_collateral_sync(fmspc.clone(), ca, now)?
pccs.get_collateral_for_quote_sync(&input)?
};

verify_dcap_attestation_with_collateral_and_timestamp(
Expand Down Expand Up @@ -121,22 +149,12 @@ pub async fn verify_dcap_attestation_with_given_timestamp(
) -> Result<MultiMeasurements, DcapVerificationError> {
let quote = Quote::parse(&input)?;

let ca = quote.ca()?;
let fmspc = hex::encode_upper(quote.fmspc()?);

let collateral = if let Some(given_collateral) = collateral {
given_collateral
} else if let Some(ref pccs) = pccs_option {
let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?;
collateral
pccs.get_collateral_for_quote(&input).await?
} else {
get_collateral_for_fmspc(
PCS_URL,
fmspc.clone(),
ca,
false, // Indicates not SGX
)
.await?
CollateralClient::with_default_http(PCS_URL)?.fetch(&input).await?
};

verify_dcap_attestation_with_collateral_and_timestamp(
Expand All @@ -159,8 +177,6 @@ fn verify_dcap_attestation_with_collateral_and_timestamp(
) -> Result<MultiMeasurements, DcapVerificationError> {
tracing::info!("Verifying DCAP attestation: {quote:?}");

let fmspc = hex::encode_upper(quote.fmspc()?);

// Override outdated TCB only if we are on Azure and the FMSPC is known to
// be outdated
let override_outdated_tcb = if override_azure_outdated_tcb {
Expand Down Expand Up @@ -190,7 +206,6 @@ fn verify_dcap_attestation_with_collateral_and_timestamp(
tracing::warn!(
status = %verified_report.status,
advisory_ids = ?verified_report.advisory_ids,
fmspc,
"DCAP verification succeeded with non-UpToDate TCB status"
);
}
Expand All @@ -208,20 +223,11 @@ fn verify_dcap_attestation_with_collateral_and_timestamp(
pub async fn verify_dcap_attestation(
input: Vec<u8>,
expected_input_data: [u8; 64],
pccs: Option<Pccs>,
_pccs: Option<Pccs>,
) -> Result<MultiMeasurements, DcapVerificationError> {
let quote = Quote::parse(&input)?;
let ca = quote.ca()?;
let fmspc = hex::encode_upper(quote.fmspc()?);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
let collateral = if let Some(ref pccs) = pccs {
let (collateral, _is_fresh) = pccs.get_collateral(fmspc, ca, now).await?;
collateral
} else {
mock_tdx::mock_collateral()
};
let verifier = mock_tdx::mock_dcap_verifier();
verifier.verify(&input, &collateral, now)?;
let evidence = parse_dcap_evidence(&input)?;
let quote = Quote::parse(&evidence.quote)?;
let _collateral = evidence.collateral;

let measurements = MultiMeasurements::from_dcap_qvl_quote(&quote)?;
if get_quote_input_data(quote.report) != expected_input_data {
Expand All @@ -235,15 +241,11 @@ pub async fn verify_dcap_attestation(
pub fn verify_dcap_attestation_sync(
input: Vec<u8>,
expected_input_data: [u8; 64],
pccs: Pccs,
_pccs: Pccs,
) -> Result<MultiMeasurements, DcapVerificationError> {
let quote = Quote::parse(&input)?;
let ca = quote.ca()?;
let fmspc = hex::encode_upper(quote.fmspc()?);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
let collateral = pccs.get_collateral_sync(fmspc, ca, now)?;
let verifier = mock_tdx::mock_dcap_verifier();
verifier.verify(&input, &collateral, now)?;
let evidence = parse_dcap_evidence(&input)?;
let quote = Quote::parse(&evidence.quote)?;
let _collateral = evidence.collateral;

let measurements = MultiMeasurements::from_dcap_qvl_quote(&quote)?;
if get_quote_input_data(quote.report.clone()) != expected_input_data {
Expand Down Expand Up @@ -290,6 +292,8 @@ pub enum DcapVerificationError {
Pccs(#[from] PccsError),
#[error("Timestamp exceeds i64 range")]
TimeStampExceedsI64,
#[error("DCAP evidence JSON: {0}")]
SerdeJson(#[from] serde_json::Error),
}

#[cfg(test)]
Expand Down Expand Up @@ -397,7 +401,7 @@ mod tests {
.unwrap();
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_mock_dcap_verify_uses_pccs_when_provided() {
let mock_pcs = spawn_mock_pcs_server(MockPcsConfig {
include_fmspcs_listing: false,
Expand All @@ -407,7 +411,7 @@ mod tests {
.unwrap();
let pccs = Pccs::new(Some(mock_pcs.base_url.clone()));
let expected_input_data = [0xA5; 64];
let attestation_bytes = create_dcap_attestation(expected_input_data).unwrap();
let attestation_bytes = create_dcap_attestation(expected_input_data, Some(&pccs)).unwrap();

let measurements =
verify_dcap_attestation(attestation_bytes, expected_input_data, Some(pccs))
Expand Down
Loading
Loading