Skip to content
Open
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
3 changes: 2 additions & 1 deletion dstack/ra-tls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ rmp-serde.workspace = true
quote = ["dstack-attest/quote"]

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
tokio = { workspace = true, features = ["full"] }
mock-attestation = { path = "../crates/mock-attestation" }
269 changes: 269 additions & 0 deletions dstack/ra-tls/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,29 @@ async fn verify_cert(
.verify_with_ra_pubkey(&public_key_der, verifier)
.await
.context("RA-TLS attestation verification failed")?;
if app_id.is_some() || app_info.is_some() {
let attested = attestation
.decode_app_info(false)
.context("certificate identity extensions require attested app info")?;
if let Some(extension) = &app_id {
if extension != &attested.app_id {
bail!("certificate app-id extension does not match attested app id");
}
}
if let Some(extension) = &app_info {
let matches = extension.app_id == attested.app_id
&& extension.compose_hash == attested.compose_hash
&& extension.instance_id == attested.instance_id
&& extension.device_id == attested.device_id
&& extension.mr_system == attested.mr_system
&& extension.mr_aggregated == attested.mr_aggregated
&& extension.os_image_hash == attested.os_image_hash
&& extension.key_provider_info == attested.key_provider_info;
if !matches {
bail!("certificate app-info extension does not match attested app info");
}
}
}
Ok(VerifiedRaTlsCert {
public_key_der,
attestation,
Expand Down Expand Up @@ -158,6 +181,252 @@ mod tests {
.into_versioned()
}

#[tokio::test]
async fn ra_certificate_profile_quote_key_and_app_mutation_matrix() {
use std::{sync::Arc, time::Duration};

use cc_eventlog::{EventLogVersion, RuntimeEvent};
use mock_attestation::server::{serve_listener, MockCollateralState};

let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let pccs = format!("http://{address}");
let state = Arc::new(MockCollateralState::from_seed([0x74; 32], &pccs).unwrap());
let server = tokio::spawn(serve_listener(listener, state.clone()));
let verifier = AttestationVerifier::new_with_tdx_root(
Some(&dstack_types::CollateralUrls {
pccs: Some(pccs),
..Default::default()
}),
state.tdx.root_ca_pem().as_bytes(),
)
.unwrap();

let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let report_data = QuoteContentType::RaTlsCert.to_report_data(&key.public_key_der());
let events = vec![
RuntimeEvent::new("app-id".into(), vec![0x11; 20], EventLogVersion::V1),
RuntimeEvent::new("compose-hash".into(), vec![0x22; 32], EventLogVersion::V1),
RuntimeEvent::new("instance-id".into(), vec![0x33; 20], EventLogVersion::V1),
RuntimeEvent::new(
"key-provider".into(),
b"fixture-provider".to_vec(),
EventLogVersion::V1,
),
];
let replayed = cc_eventlog::replay_events::<ez_hash::Sha384>(&events, None);
let mut rtmrs = [[0u8; 48]; 4];
rtmrs[3].copy_from_slice(&replayed);
let evidence = state.tdx.attest_with_rtmrs(report_data, rtmrs).unwrap();
let attestation = Attestation {
quote: AttestationQuote::DstackTdx(TdxQuote {
quote: evidence.quote,
event_log: events.iter().cloned().map(Into::into).collect(),
}),
runtime_events: events,
report_data,
config: format!(r#"{{"os_image_hash":"{}"}}"#, "44".repeat(32)),
report: (),
}
.into_versioned();
let verified_attestation = attestation
.clone()
.into_v1()
.verify(&verifier)
.await
.unwrap();
let app_info = verified_attestation.decode_app_info(false).unwrap();
let alt_names = vec!["guest.example".to_string()];
let cert = CertRequest::builder()
.key(&key)
.subject("guest.example")
.alt_names(&alt_names)
.usage_server_auth(true)
.app_id(&app_info.app_id)
.app_info(&app_info)
.attestation(&attestation)
.build()
.self_signed()
.unwrap();
let valid = verify_der(cert.der().as_ref(), &verifier).await.unwrap();
assert_eq!(valid.app_id.as_deref(), Some(app_info.app_id.as_slice()));

let mut changed_app_info = app_info.clone();
changed_app_info.os_image_hash[0] ^= 1;
let cert = CertRequest::builder()
.key(&key)
.subject("guest.example")
.alt_names(&alt_names)
.usage_server_auth(true)
.app_id(&app_info.app_id)
.app_info(&changed_app_info)
.attestation(&attestation)
.build()
.self_signed()
.unwrap();
let error = verify_der(cert.der().as_ref(), &verifier)
.await
.err()
.unwrap();
assert!(format!("{error:#}").contains("app-info extension does not match"));

let mut changed_app_id = app_info.app_id.clone();
changed_app_id[0] ^= 1;
let cert = CertRequest::builder()
.key(&key)
.subject("guest.example")
.alt_names(&alt_names)
.usage_server_auth(true)
.app_id(&changed_app_id)
.app_info(&app_info)
.attestation(&attestation)
.build()
.self_signed()
.unwrap();
let error = verify_der(cert.der().as_ref(), &verifier)
.await
.err()
.unwrap();
assert!(format!("{error:#}").contains("app-id extension does not match"));

let mut changed_quote = attestation.clone();
let VersionedAttestation::V0 {
attestation: changed,
} = &mut changed_quote
else {
unreachable!("V1 runtime events must use the legacy-compatible container")
};
let AttestationQuote::DstackTdx(tdx_quote) = &mut changed.quote else {
unreachable!()
};
tdx_quote.quote[100] ^= 1;
let cert = CertRequest::builder()
.key(&key)
.subject("guest.example")
.alt_names(&alt_names)
.usage_server_auth(true)
.attestation(&changed_quote)
.build()
.self_signed()
.unwrap();
assert!(verify_der(cert.der().as_ref(), &verifier).await.is_err());

let wrong_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let cert = CertRequest::builder()
.key(&wrong_key)
.subject("guest.example")
.alt_names(&alt_names)
.usage_server_auth(true)
.attestation(&attestation)
.build()
.self_signed()
.unwrap();
assert!(format!(
"{:#}",
verify_der(cert.der().as_ref(), &verifier)
.await
.err()
.unwrap()
)
.contains("report data mismatch"));

let no_san = CertRequest::builder()
.key(&key)
.subject("guest.example")
.usage_server_auth(true)
.attestation(&attestation)
.build()
.self_signed()
.unwrap();
assert!(format!(
"{:#}",
verify_der(no_san.der().as_ref(), &verifier)
.await
.err()
.unwrap()
)
.contains("SAN extension missing"));

let no_eku = CertRequest::builder()
.key(&key)
.subject("guest.example")
.alt_names(&alt_names)
.attestation(&attestation)
.build()
.self_signed()
.unwrap();
assert!(format!(
"{:#}",
verify_der(no_eku.der().as_ref(), &verifier)
.await
.err()
.unwrap()
)
.contains("extended key usage extension missing"));

let profile_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let mut params = rcgen::CertificateParams::new(alt_names.clone()).unwrap();
params.key_usages = vec![rcgen::KeyUsagePurpose::KeyEncipherment];
params
.extended_key_usages
.push(rcgen::ExtendedKeyUsagePurpose::ServerAuth);
let bad_usage = params.self_signed(&profile_key).unwrap();
let (_, parsed) = x509_parser::parse_x509_certificate(bad_usage.der()).unwrap();
assert!(verify_certificate_profile(&parsed)
.unwrap_err()
.to_string()
.contains("does not permit digital signatures"));

let now = std::time::SystemTime::now();
let expired = CertRequest::builder()
.key(&key)
.subject("guest.example")
.alt_names(&alt_names)
.usage_server_auth(true)
.not_before(now - Duration::from_secs(2 * 86400))
.not_after(now - Duration::from_secs(86400))
.attestation(&attestation)
.build()
.self_signed()
.unwrap();
assert!(format!(
"{:#}",
verify_der(expired.der().as_ref(), &verifier)
.await
.err()
.unwrap()
)
.contains("outside its validity period"));

let mut bad_signature = valid_cert_bytes(&key, &alt_names, &attestation);
let last = bad_signature.len() - 1;
bad_signature[last] ^= 1;
assert!(format!(
"{:#}",
verify_der(&bad_signature, &verifier).await.err().unwrap()
)
.contains("self-signature verification failed"));
server.abort();
}

fn valid_cert_bytes(
key: &KeyPair,
alt_names: &[String],
attestation: &VersionedAttestation,
) -> Vec<u8> {
CertRequest::builder()
.key(key)
.subject("guest.example")
.alt_names(alt_names)
.usage_server_auth(true)
.attestation(attestation)
.build()
.self_signed()
.unwrap()
.der()
.to_vec()
}

#[tokio::test]
async fn verify_der_rejects_missing_attestation_extension() {
let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
Expand Down
Loading