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
18 changes: 16 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 17 additions & 16 deletions crates/attestation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,30 @@ repository = "https://github.com/flashbots/attested-tls"
keywords = ["attestation", "CVM", "TDX"]

[dependencies]
dcap-qvl = { workspace = true, features = ["danger-allow-tcb-override"] }
pccs = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
tokio-rustls = { workspace = true, default-features = false }
x509-parser = "0.18.0"
thiserror = "2.0.17"

anyhow = "1.0.100"
pem-rfc7468 = { version = "0.7.0", features = ["std"] }
base64 = "0.22.1"
configfs-tsm = "0.0.2"
rand_core = { version = "0.6.4", features = ["getrandom"] }
dcap-qvl = { workspace = true, features = ["danger-allow-tcb-override"] }
hex = "0.4.3"
http = "1.3.1"
serde_json = "1.0.145"
num-bigint = "0.4.6"
once_cell = "1.21.3"
parity-scale-codec = "3.7.5"
pem-rfc7468 = { version = "0.7.0", features = ["std"] }
rand_core = { version = "0.6.4", features = ["getrandom"] }
reqwest = { version = "0.12.23", default-features = false, features = [ "rustls-tls-webpki-roots-no-provider" ] }
serde = "1.0.228"
base64 = "0.22.1"
reqwest = { version = "0.12.23", default-features = false, features = [
"rustls-tls-webpki-roots-no-provider",
] }
serde_json = "1.0.145"
thiserror = "2.0.17"
time = "0.3.47"
tracing = "0.1.41"
parity-scale-codec = "3.7.5"
num-bigint = "0.4.6"
ureq = "2.12.1"
webpki = { package = "rustls-webpki", version = "0.103.8" }
time = "0.3.47"
once_cell = "1.21.3"
x509-parser = "0.18.0"

# Used for azure vTPM attestation support
az-tdx-vtpm = { version = "0.7.4", optional = true }
Expand All @@ -42,10 +42,11 @@ openssl = { version = "0.10.75", optional = true }
tdx-quote = { version = "0.0.5", features = ["mock"], optional = true }

[dev-dependencies]
tempfile = "3.23.0"
tdx-quote = { version = "0.0.5", features = ["mock"] }
tokio-rustls = { workspace = true, default-features = true }

serde-saphyr = "0.0.22"
tdx-quote = { version = "0.0.5", features = ["mock"] }
tempfile = "3.23.0"

[features]
default = []
Expand Down
32 changes: 13 additions & 19 deletions crates/attestation/src/azure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,38 +396,32 @@ impl RsaPubKey {
}

/// Detect whether we are on Azure and can make an Azure vTPM attestation
pub async fn detect_azure_cvm() -> Result<bool, MaaError> {
let client = reqwest::Client::builder().no_proxy().timeout(Duration::from_secs(2)).build()?;

let response = match client.get(AZURE_METADATA_API).header("Metadata", "true").send().await {
Ok(response) => response,
pub fn detect_azure_cvm() -> Result<bool, MaaError> {
let agent = ureq::AgentBuilder::new().timeout(Duration::from_millis(200)).build();
let resp = match agent.get(AZURE_METADATA_API).set("Metadata", "true").call() {
Ok(resp) => resp,
Err(err) => {
tracing::debug!("Azure CVM detection failed: Azure metadata API request failed: {err}");
return Ok(false);
}
};

if !response.status().is_success() {
if !resp.status() != 200 {
tracing::debug!(
"Azure CVM detection failed: metadata API returned non-success status: {}",
response.status()
resp.status()
);
return Ok(false);
}

// Ensure the response has a JSON content type
let content_type = response
.headers()
.get(CONTENT_TYPE)
.map(|value| value.to_str().map(str::to_owned))
.transpose()
.map_err(|_| MaaError::AzureMetadataApiNonJsonResponse { content_type: None })?;

if !content_type
.as_deref()
.is_some_and(|value| value.to_lowercase().starts_with("application/json"))
{
return Err(MaaError::AzureMetadataApiNonJsonResponse { content_type });
let content_type = resp
.header(CONTENT_TYPE.as_str())
.map(|value| value.to_owned())
.ok_or_else(|| MaaError::AzureMetadataApiNonJsonResponse { content_type: None })?;

if !content_type.to_lowercase().starts_with("application/json") {
return Err(MaaError::AzureMetadataApiNonJsonResponse { content_type: Some(content_type) });
}

match az_tdx_vtpm::is_tdx_cvm() {
Expand Down
74 changes: 38 additions & 36 deletions crates/attestation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod measurements;

use std::{
fmt::{self, Display, Formatter},
io::Read,
net::IpAddr,
time::{Duration, SystemTime, UNIX_EPOCH},
};
Expand Down Expand Up @@ -54,7 +55,7 @@ impl AttestationExchangeMessage {
Err(AttestationError::AttestationTypeNotSupported)
}
}
_ => {
AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => {
Comment thread
0x416e746f6e marked this conversation as resolved.
#[cfg(any(test, feature = "mock"))]
{
let quote = tdx_quote::Quote::from_bytes(&self.attestation)
Expand Down Expand Up @@ -103,18 +104,18 @@ impl AttestationType {
}

/// Detect what platform we are on by attempting an attestation
pub async fn detect() -> Result<Self, AttestationError> {
pub fn detect() -> Result<Self, AttestationError> {
// First attempt azure, if the feature is present
#[cfg(feature = "azure")]
{
if azure::detect_azure_cvm().await? {
if azure::detect_azure_cvm()? {
return Ok(AttestationType::AzureTdx);
}
}
// Otherwise try DCAP quote - this internally checks that the quote provider
// is `tdx_guest`
if configfs_tsm::create_tdx_quote([0; 64]).is_ok() {
if running_on_gcp().await? {
if running_on_gcp()? {
return Ok(AttestationType::GcpTdx);
} else {
return Ok(AttestationType::DcapTdx);
Expand Down Expand Up @@ -170,8 +171,8 @@ impl AttestationGenerator {

/// Detect what confidential compute platform is present and create the
/// appropriate attestation generator
pub async fn detect() -> Result<Self, AttestationError> {
Self::new_with_detection(None, None).await
pub fn detect() -> Result<Self, AttestationError> {
Self::new_with_detection(None, None)
}

/// Do not generate attestations
Expand All @@ -181,7 +182,7 @@ impl AttestationGenerator {

/// Create an [AttestationGenerator] detecting the attestation type if
/// it is not given
pub async fn new_with_detection(
pub fn new_with_detection(
attestation_type_string: Option<String>,
attestation_provider_url: Option<String>,
) -> Result<Self, AttestationError> {
Expand All @@ -196,7 +197,7 @@ impl AttestationGenerator {
let attestation_type_string = attestation_type_string.unwrap_or_else(|| "auto".to_string());
let attestation_type = if attestation_type_string == "auto" {
tracing::info!("Doing attestation type detection...");
AttestationType::detect().await?
AttestationType::detect()?
} else {
serde_json::from_value(serde_json::Value::String(attestation_type_string))?
};
Expand All @@ -206,12 +207,12 @@ impl AttestationGenerator {
}

/// Generate an attestation exchange message with given input data
pub async fn generate_attestation(
pub fn generate_attestation(
&self,
input_data: [u8; 64],
) -> Result<AttestationExchangeMessage, AttestationError> {
if let Some(url) = &self.attestation_provider_url {
Self::use_attestation_provider(url, self.attestation_type, input_data).await
Self::use_attestation_provider(url, self.attestation_type, input_data)
} else {
Ok(AttestationExchangeMessage {
attestation_type: self.attestation_type,
Expand Down Expand Up @@ -241,33 +242,37 @@ impl AttestationGenerator {
Err(AttestationError::AttestationTypeNotSupported)
}
}
_ => dcap::create_dcap_attestation(input_data),
AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => {
dcap::create_dcap_attestation(input_data)
}
}
}

/// Generate an attestation by using an external service for the
/// attestation generation
async fn use_attestation_provider(
fn use_attestation_provider(
url: &str,
attestation_type: AttestationType,
input_data: [u8; 64],
) -> Result<AttestationExchangeMessage, AttestationError> {
let url = format!("{}/attest/{}", url, hex::encode(input_data));

let response = reqwest::get(url)
.await
let mut response = ureq::get(&url)
.timeout(Duration::from_millis(1000))
.call()
.map_err(|err| AttestationError::AttestationProvider(err.to_string()))?
.bytes()
.await
.map_err(|err| AttestationError::AttestationProvider(err.to_string()))?
.to_vec();
.into_reader();
let mut body = Vec::new();
response
.read_to_end(&mut body)
.map_err(|err| AttestationError::AttestationProvider(err.to_string()))?;

// If the response is not already wrapped in an attestation exchange
// message, wrap it in one
if let Ok(message) = AttestationExchangeMessage::decode(&mut &response[..]) {
if let Ok(message) = AttestationExchangeMessage::decode(&mut &body[..]) {
Ok(message)
} else {
Ok(AttestationExchangeMessage { attestation_type, attestation: response })
Ok(AttestationExchangeMessage { attestation_type, attestation: body })
}
}
}
Expand Down Expand Up @@ -391,7 +396,7 @@ impl AttestationVerifier {
return Err(AttestationError::AttestationTypeNotSupported);
}
}
_ => {
AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => {
dcap::verify_dcap_attestation(
attestation_exchange_message.attestation,
expected_input_data,
Expand Down Expand Up @@ -497,14 +502,13 @@ fn log_attestation(attestation: &AttestationExchangeMessage) {

/// Test whether it looks like we are running on GCP by hitting the metadata
/// API
async fn running_on_gcp() -> Result<bool, AttestationError> {
let client = reqwest::Client::builder().timeout(Duration::from_millis(200)).build()?;

let resp = client.get(GCP_METADATA_API).send().await;
fn running_on_gcp() -> Result<bool, AttestationError> {
let agent = ureq::AgentBuilder::new().timeout(Duration::from_millis(200)).build();
let resp = agent.get(GCP_METADATA_API).call();

if let Ok(r) = resp {
return Ok(r.status().is_success() &&
r.headers().get("Metadata-Flavor").map(|v| v == "Google").unwrap_or(false));
return Ok(r.status() == 200 &&
r.header("Metadata-Flavor").map(|v| v == "Google").unwrap_or(false));
}

Ok(false)
Expand Down Expand Up @@ -632,19 +636,19 @@ mod tests {
addr
}

#[tokio::test]
async fn attestation_detection_does_not_panic() {
#[test]
fn attestation_detection_does_not_panic() {
// We dont enforce what platform the test is run on, only that the function
// does not panic
let _ = AttestationGenerator::new_with_detection(None, None).await;
let _ = AttestationGenerator::new_with_detection(None, None);
}

#[tokio::test]
async fn running_on_gcp_check_does_not_panic() {
let _ = running_on_gcp().await;
#[test]
fn running_on_gcp_check_does_not_panic() {
let _ = running_on_gcp();
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn attestation_provider_response_is_wrapped_if_needed() {
let input_data = [0u8; 64];

Expand All @@ -661,7 +665,6 @@ mod tests {
AttestationType::GcpTdx,
input_data,
)
.await
.unwrap();
assert_eq!(decoded.attestation_type, AttestationType::None);
assert_eq!(decoded.attestation, vec![1, 2, 3]);
Expand All @@ -673,7 +676,6 @@ mod tests {
AttestationType::DcapTdx,
input_data,
)
.await
.unwrap();
assert_eq!(wrapped.attestation_type, AttestationType::DcapTdx);
assert_eq!(wrapped.attestation, vec![9, 8]);
Expand Down
Loading
Loading