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
227 changes: 192 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-no
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" }
4 changes: 2 additions & 2 deletions crates/attestation-provider-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use parity_scale_codec::{Decode, Encode};
use parity_scale_codec::Encode;
use tokio::net::TcpListener;

#[derive(Clone)]
Expand Down Expand Up @@ -56,7 +56,7 @@ pub async fn attestation_provider_client(
.bytes()
.await?;

let remote_attestation_message = AttestationExchangeMessage::decode(&mut &response[..])?;
let remote_attestation_message = AttestationExchangeMessage::decode_compatible(&response)?;
let remote_attestation_type = remote_attestation_message.attestation_type();

println!("Remote attestation type: {remote_attestation_type}");
Expand Down
36 changes: 33 additions & 3 deletions crates/attestation-provider-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ enum CliCommand {
/// given)
#[arg(long)]
server_attestation_type: Option<String>,
/// PCCS URL used to fetch collateral bundled with generated
/// attestations. Defaults to Intel PCS.
#[arg(long, env = "PCCS_URL")]
pccs_url: Option<String>,
},
Client {
/// Socket address of a attestation provider server
Expand Down Expand Up @@ -75,10 +79,13 @@ async fn main() -> anyhow::Result<()> {
}

match cli.command {
CliCommand::Server { listen_addr, server_attestation_type } => {
CliCommand::Server { listen_addr, server_attestation_type, pccs_url } => {
let none_requested = server_attestation_type.as_deref() == Some("none");
let attestation_generator =
AttestationGenerator::new_with_detection(server_attestation_type, None)?;
let attestation_generator = AttestationGenerator::new_with_detection_and_pccs_url(
server_attestation_type,
None,
pccs_url,
)?;

if attestation_generator.attestation_type == AttestationType::None && !none_requested {
anyhow::bail!(
Expand Down Expand Up @@ -109,3 +116,26 @@ async fn main() -> anyhow::Result<()> {

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn server_accepts_pccs_url() {
let cli = Cli::try_parse_from([
"attestation-provider-server",
"server",
"--server-attestation-type",
"dcap-tdx",
"--pccs-url",
"http://127.0.0.1:8081",
])
.unwrap();

let CliCommand::Server { pccs_url, .. } = cli.command else {
panic!("expected server command");
};
assert_eq!(pccs_url.as_deref(), Some("http://127.0.0.1:8081"));
}
}
26 changes: 14 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.
TDX attestation generation uses the [`pccs`](../pccs) crate to fetch and bundle
the collateral required to verify the quote. This includes the DCAP quote
embedded in Azure vTPM evidence. Verification consumes bundled collateral, so
verifier-side network fetching is not required for the normal 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 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 @@ -82,11 +82,13 @@ 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, GCP TDX, or Azure TDX attestations, Intel PCS is used to
retrieve collateral unless a PCCS URL is configured. The evidence and
collateral are serialized together. Verifiers use their PCCS configuration
only as a fallback when evidence does not include collateral. 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
116 changes: 76 additions & 40 deletions crates/attestation/src/azure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use base64::{Engine as _, engine::general_purpose::URL_SAFE as BASE64_URL_SAFE};
use dcap_qvl::QuoteCollateralV3;
use num_bigint::BigUint;
use openssl::{error::ErrorStack, pkey::PKey};
use pccs::Pccs;
use pccs::{Pccs, PccsError};
use reqwest::header::CONTENT_TYPE;
use serde::{Deserialize, Serialize};
use thiserror::Error;
Expand Down Expand Up @@ -130,18 +130,23 @@ struct PreparedAzureAttestation {
tpm_attestation: TpmAttest,
}

/// Generate a TDX attestation on Azure.
/// Generate a TDX attestation on Azure and bundle the Intel DCAP
/// collateral needed to verify its embedded TDX quote.
///
/// This may perform network calls. Azure's IMDS is queried for the TDX
/// quote, and the vTPM AK certificate's Authority Information Access (AIA)
/// CA Issuers URLs are followed to include the observed issuer
/// intermediates in the evidence.
/// This performs network calls. Azure's IMDS is queried for the TDX quote,
/// PCCS (or Intel PCS by default) is queried for DCAP collateral, and the
/// vTPM AK certificate's Authority Information Access (AIA) CA Issuers URLs
/// are followed to include the observed issuer intermediates in the
/// evidence.
///
/// The intermediates are included as untrusted evidence so verifiers do not
/// need network access or AIA-fetching logic. This keeps verification
/// deterministic and easier to reuse in constrained verifier environments
/// such as TEEs, onchain verification, or zero-knowledge proof generation.
pub fn create_azure_attestation(input_data: [u8; 64]) -> Result<Vec<u8>, MaaError> {
pub fn create_azure_attestation(
input_data: [u8; 64],
pccs: Option<&Pccs>,
) -> Result<(Vec<u8>, QuoteCollateralV3), MaaError> {
let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?;

let hcl = hcl::HclReport::new(hcl_report_bytes.clone())?;
Expand All @@ -151,6 +156,15 @@ pub fn create_azure_attestation(input_data: [u8; 64]) -> Result<Vec<u8>, MaaErro
// This makes a request to Azure Instance metadata service and gives us a
// binary response
let td_quote_bytes = imds::get_td_quote(&td_report_from_hcl)?;
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(&td_quote_bytes)?;

let ak_certificate_der = read_ak_certificate_from_tpm()?;
let (remaining_bytes, ak_leaf_certificate) = X509Certificate::from_der(&ak_certificate_der)?;
Expand Down Expand Up @@ -188,14 +202,15 @@ pub fn create_azure_attestation(input_data: [u8; 64]) -> Result<Vec<u8>, MaaErro
// If this ever fails, then we have a problem and probably just need to
// increase MAX_AZURE_ATTESTATION_PAYLOAD_SIZE
ensure_azure_attestation_payload_size(&attestation_json)?;
Ok(attestation_json)
Ok((attestation_json, collateral))
}

/// Verify a TDX attestation from Azure
pub async fn verify_azure_attestation(
input: Vec<u8>,
expected_input_data: [u8; 64],
pccs: Option<Pccs>,
collateral: Option<QuoteCollateralV3>,
override_azure_outdated_tcb: bool,
) -> Result<super::measurements::MultiMeasurements, MaaError> {
let now = unix_time_now_secs()?;
Expand All @@ -204,7 +219,7 @@ pub async fn verify_azure_attestation(
input,
expected_input_data,
pccs,
None,
collateral,
now,
override_azure_outdated_tcb,
)
Expand All @@ -213,22 +228,24 @@ pub async fn verify_azure_attestation(

/// Verify a TDX attestation from Azure - synchronous version
///
/// This relies on having DCAP collateral already present in the cache
///
/// If possible, prefer the async version
/// Bundled collateral is used when provided. Otherwise collateral is
/// fetched from the supplied PCCS, or Intel PCS by default.
pub fn verify_azure_attestation_sync(
input: Vec<u8>,
expected_input_data: [u8; 64],
pccs: Pccs,
pccs: Option<Pccs>,
collateral: Option<QuoteCollateralV3>,
override_azure_outdated_tcb: bool,
) -> Result<super::measurements::MultiMeasurements, MaaError> {
let now = unix_time_now_secs()?;

let pccs = pccs.unwrap_or_else(|| Pccs::new_without_prewarm(None));

verify_azure_attestation_with_given_timestamp_sync(
input,
expected_input_data,
pccs,
None,
collateral,
now,
override_azure_outdated_tcb,
)
Expand Down Expand Up @@ -614,6 +631,8 @@ pub enum MaaError {
ClaimsUserDataInputMismatch,
#[error("DCAP verification: {0}")]
DcapVerification(#[from] crate::dcap::DcapVerificationError),
#[error("PCCS: {0}")]
Pccs(#[from] PccsError),
#[error(
"Azure metadata API returned a successful response with non-JSON content-type: {content_type:?}"
)]
Expand All @@ -622,10 +641,7 @@ pub enum MaaError {

#[cfg(test)]
mod test_utils {
use base64::{Engine as _, engine::general_purpose::URL_SAFE as BASE64_URL_SAFE};

use super::{AttestationDocument, create_azure_attestation};
use crate::dcap::PCS_URL;

/// Capture a complete Azure TDX attestation fixture from inside an
/// Azure TDX CVM.
Expand Down Expand Up @@ -653,27 +669,14 @@ mod test_utils {

// Keep this aligned with existing Azure fixture tests, which use zeroed
// report input data.
let attestation_json = create_azure_attestation([0u8; 64]).unwrap();
let (attestation_json, collateral) = create_azure_attestation([0u8; 64], None).unwrap();
let attestation_document: AttestationDocument =
serde_json::from_slice(&attestation_json).unwrap();

let intermediate_count =
attestation_document.tpm_attestation.ak_intermediate_certificates_pem.len();
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 timestamp =
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
let attestation_path =
Expand All @@ -698,14 +701,22 @@ 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}");
}
}

#[cfg(test)]
mod tests {
use parity_scale_codec::Encode;

use super::*;
use crate::{
AttestationEvidence,
AttestationEvidenceWithCollateral,
AttestationExchangeMessage,
AttestationType,
mock_platform_metadata,
};

fn input_data_from_attestation(attestation_bytes: &[u8]) -> [u8; 64] {
let attestation_document: AttestationDocument =
Expand All @@ -728,6 +739,35 @@ mod tests {
}
}

#[test]
fn azure_evidence_with_collateral_round_trips_through_scale() {
let attestation_document: AttestationDocument = serde_saphyr::from_slice(include_bytes!(
"../../test-assets/azure-tdx-with-ak-intermediates-1780922561.yaml"
))
.unwrap();
let attestation_json = serde_json::to_vec(&attestation_document).unwrap();
let collateral = serde_saphyr::from_slice(include_bytes!(
"../../test-assets/azure-collateral-with-ak-intermediates-1780922561.yaml"
))
.unwrap();
let message = AttestationExchangeMessage {
attestation_evidence: Some(AttestationEvidenceWithCollateral {
evidence: AttestationEvidence {
quote: attestation_json.clone(),
platform: mock_platform_metadata(AttestationType::AzureTdx).unwrap(),
},
collateral: Some(collateral),
}),
};

let encoded = message.encode();
let decoded = AttestationExchangeMessage::decode_compatible(&encoded).unwrap();
assert_eq!(decoded.attestation_type(), AttestationType::AzureTdx);
let evidence = decoded.attestation_evidence.unwrap();
assert_eq!(evidence.evidence.quote, attestation_json);
assert!(evidence.collateral.is_some());
}

#[test]
fn get_measurements_rejects_oversized_payload_before_deserialize() {
let actual = MAX_AZURE_ATTESTATION_PAYLOAD_SIZE + 1;
Expand All @@ -749,16 +789,12 @@ mod tests {
let actual = MAX_AZURE_ATTESTATION_PAYLOAD_SIZE + 1;
let input = vec![b'{'; actual];

let err = verify_azure_attestation(input.clone(), [0; 64], None, false).await.unwrap_err();
let err =
verify_azure_attestation(input.clone(), [0; 64], None, None, false).await.unwrap_err();
assert_payload_too_large(err, actual);

let err = verify_azure_attestation_sync(
input.clone(),
[0; 64],
Pccs::new_without_prewarm(None),
false,
)
.unwrap_err();
let err =
verify_azure_attestation_sync(input.clone(), [0; 64], None, None, false).unwrap_err();
assert_payload_too_large(err, actual);

let err = verify_azure_attestation_with_given_timestamp(
Expand Down
Loading
Loading