From 03b2efeed662e90573a7caa8f5c4401c1bf335a9 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 29 Jul 2026 16:01:04 +0000 Subject: [PATCH 1/3] fix(ra-tls): validate certificate security profile --- dstack/ra-tls/Cargo.toml | 2 +- dstack/ra-tls/src/attestation.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/dstack/ra-tls/Cargo.toml b/dstack/ra-tls/Cargo.toml index 3725f3330..fbc1b9708 100644 --- a/dstack/ra-tls/Cargo.toml +++ b/dstack/ra-tls/Cargo.toml @@ -24,7 +24,7 @@ rustls-pki-types.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true -x509-parser.workspace = true +x509-parser = { workspace = true, features = ["verify"] } yasna.workspace = true tracing.workspace = true sha3.workspace = true diff --git a/dstack/ra-tls/src/attestation.rs b/dstack/ra-tls/src/attestation.rs index 6d120020a..5bdc4445f 100644 --- a/dstack/ra-tls/src/attestation.rs +++ b/dstack/ra-tls/src/attestation.rs @@ -53,11 +53,42 @@ pub async fn verify_pem(cert: &[u8], verifier: &AttestationVerifier) -> Result) -> Result<()> { + cert.verify_signature(None) + .context("certificate self-signature verification failed")?; + if !cert.validity().is_valid() { + bail!("certificate is outside its validity period"); + } + let key_usage = cert + .key_usage() + .context("failed to decode certificate key usage")? + .context("certificate key usage extension missing")?; + if !key_usage.value.digital_signature() { + bail!("certificate key usage does not permit digital signatures"); + } + let extended = cert + .extended_key_usage() + .context("failed to decode certificate extended key usage")? + .context("certificate extended key usage extension missing")?; + if !extended.value.server_auth && !extended.value.client_auth { + bail!("certificate extended key usage permits neither server nor client authentication"); + } + let san = cert + .subject_alternative_name() + .context("failed to decode certificate SAN")? + .context("certificate SAN extension missing")?; + if san.value.general_names.is_empty() { + bail!("certificate SAN extension is empty"); + } + Ok(()) +} + /// Verify the RA-TLS attestation embedded in a parsed X.509 certificate. async fn verify_cert( cert: &x509_parser::prelude::X509Certificate<'_>, verifier: &AttestationVerifier, ) -> Result { + verify_certificate_profile(cert)?; let attestation = from_cert(cert)?.context("RA-TLS attestation extension missing")?; let public_key_der = cert.tbs_certificate.public_key().raw.to_vec(); if public_key_der.is_empty() { From 600189134d392ff8a4ba35785cbb66012ab1eafc Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 29 Jul 2026 16:01:42 +0000 Subject: [PATCH 2/3] fix(test): satisfy RA certificate SAN profile --- dstack/ra-tls/src/attestation.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dstack/ra-tls/src/attestation.rs b/dstack/ra-tls/src/attestation.rs index 5bdc4445f..01b15afed 100644 --- a/dstack/ra-tls/src/attestation.rs +++ b/dstack/ra-tls/src/attestation.rs @@ -161,9 +161,11 @@ mod tests { #[tokio::test] async fn verify_der_rejects_missing_attestation_extension() { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let alt_names = vec!["missing-attestation.example".to_string()]; let cert = CertRequest::builder() .key(&key) .subject("missing-attestation.example") + .alt_names(&alt_names) .usage_server_auth(true) .build() .self_signed() @@ -180,9 +182,11 @@ mod tests { async fn verify_der_rejects_attestation_not_bound_to_cert_key() { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let attestation = fake_tdx_attestation([0u8; 64]); + let alt_names = vec!["mismatched-attestation.example".to_string()]; let cert = CertRequest::builder() .key(&key) .subject("mismatched-attestation.example") + .alt_names(&alt_names) .usage_server_auth(true) .attestation(&attestation) .build() From cdda5fa3046a0303134f560fe9898c2fe7534ebf Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 4 Aug 2026 19:56:26 -0700 Subject: [PATCH 3/3] refactor(verifier): own RA certificate profile policy --- dstack/Cargo.lock | 1 + dstack/ra-tls/Cargo.toml | 2 +- dstack/ra-tls/src/attestation.rs | 35 --------------------- dstack/verifier/Cargo.toml | 1 + dstack/verifier/src/main.rs | 52 ++++++++++++++++++++++++++++---- 5 files changed, 49 insertions(+), 42 deletions(-) diff --git a/dstack/Cargo.lock b/dstack/Cargo.lock index 89ce9917a..119760422 100644 --- a/dstack/Cargo.lock +++ b/dstack/Cargo.lock @@ -2295,6 +2295,7 @@ dependencies = [ "tpm-types", "tracing", "tracing-subscriber", + "x509-parser", ] [[package]] diff --git a/dstack/ra-tls/Cargo.toml b/dstack/ra-tls/Cargo.toml index fbc1b9708..3725f3330 100644 --- a/dstack/ra-tls/Cargo.toml +++ b/dstack/ra-tls/Cargo.toml @@ -24,7 +24,7 @@ rustls-pki-types.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true -x509-parser = { workspace = true, features = ["verify"] } +x509-parser.workspace = true yasna.workspace = true tracing.workspace = true sha3.workspace = true diff --git a/dstack/ra-tls/src/attestation.rs b/dstack/ra-tls/src/attestation.rs index 01b15afed..6d120020a 100644 --- a/dstack/ra-tls/src/attestation.rs +++ b/dstack/ra-tls/src/attestation.rs @@ -53,42 +53,11 @@ pub async fn verify_pem(cert: &[u8], verifier: &AttestationVerifier) -> Result) -> Result<()> { - cert.verify_signature(None) - .context("certificate self-signature verification failed")?; - if !cert.validity().is_valid() { - bail!("certificate is outside its validity period"); - } - let key_usage = cert - .key_usage() - .context("failed to decode certificate key usage")? - .context("certificate key usage extension missing")?; - if !key_usage.value.digital_signature() { - bail!("certificate key usage does not permit digital signatures"); - } - let extended = cert - .extended_key_usage() - .context("failed to decode certificate extended key usage")? - .context("certificate extended key usage extension missing")?; - if !extended.value.server_auth && !extended.value.client_auth { - bail!("certificate extended key usage permits neither server nor client authentication"); - } - let san = cert - .subject_alternative_name() - .context("failed to decode certificate SAN")? - .context("certificate SAN extension missing")?; - if san.value.general_names.is_empty() { - bail!("certificate SAN extension is empty"); - } - Ok(()) -} - /// Verify the RA-TLS attestation embedded in a parsed X.509 certificate. async fn verify_cert( cert: &x509_parser::prelude::X509Certificate<'_>, verifier: &AttestationVerifier, ) -> Result { - verify_certificate_profile(cert)?; let attestation = from_cert(cert)?.context("RA-TLS attestation extension missing")?; let public_key_der = cert.tbs_certificate.public_key().raw.to_vec(); if public_key_der.is_empty() { @@ -161,11 +130,9 @@ mod tests { #[tokio::test] async fn verify_der_rejects_missing_attestation_extension() { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let alt_names = vec!["missing-attestation.example".to_string()]; let cert = CertRequest::builder() .key(&key) .subject("missing-attestation.example") - .alt_names(&alt_names) .usage_server_auth(true) .build() .self_signed() @@ -182,11 +149,9 @@ mod tests { async fn verify_der_rejects_attestation_not_bound_to_cert_key() { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let attestation = fake_tdx_attestation([0u8; 64]); - let alt_names = vec!["mismatched-attestation.example".to_string()]; let cert = CertRequest::builder() .key(&key) .subject("mismatched-attestation.example") - .alt_names(&alt_names) .usage_server_auth(true) .attestation(&attestation) .build() diff --git a/dstack/verifier/Cargo.toml b/dstack/verifier/Cargo.toml index 99163fda6..de46a2c44 100644 --- a/dstack/verifier/Cargo.toml +++ b/dstack/verifier/Cargo.toml @@ -39,6 +39,7 @@ tar.workspace = true # Internal dependencies ra-tls.workspace = true +x509-parser = { workspace = true, features = ["verify"] } dstack-attest.workspace = true dstack-types.workspace = true dstack-mr.workspace = true diff --git a/dstack/verifier/src/main.rs b/dstack/verifier/src/main.rs index d4fd41795..47e49d299 100644 --- a/dstack/verifier/src/main.rs +++ b/dstack/verifier/src/main.rs @@ -161,6 +161,40 @@ async fn run_oneshot(file_path: &str, config: &Config) -> anyhow::Result { Ok(response.is_valid) } +fn verify_certificate_profile(cert_der: &[u8]) -> anyhow::Result<()> { + let (_, cert) = x509_parser::parse_x509_certificate(cert_der) + .context("failed to parse X.509 certificate")?; + cert.verify_signature(None) + .context("certificate self-signature verification failed")?; + if !cert.validity().is_valid() { + anyhow::bail!("certificate is outside its validity period"); + } + let key_usage = cert + .key_usage() + .context("failed to decode certificate key usage")? + .context("certificate key usage extension missing")?; + if !key_usage.value.digital_signature() { + anyhow::bail!("certificate key usage does not permit digital signatures"); + } + let extended = cert + .extended_key_usage() + .context("failed to decode certificate extended key usage")? + .context("certificate extended key usage extension missing")?; + if !extended.value.server_auth && !extended.value.client_auth { + anyhow::bail!( + "certificate extended key usage permits neither server nor client authentication" + ); + } + let san = cert + .subject_alternative_name() + .context("failed to decode certificate SAN")? + .context("certificate SAN extension missing")?; + if san.value.general_names.is_empty() { + anyhow::bail!("certificate SAN extension is empty"); + } + Ok(()) +} + async fn run_cert_oneshot(file_path: &str, config: &Config) -> anyhow::Result<()> { use std::fs; @@ -172,13 +206,19 @@ async fn run_cert_oneshot(file_path: &str, config: &Config) -> anyhow::Result<() let cert = fs::read(file_path) .map_err(|e| anyhow::anyhow!("failed to read certificate {}: {}", file_path, e))?; - let attestation_verifier = Arc::new(AttestationVerifier::load(&config.attestation)?); - let verified = if cert.starts_with(b"-----BEGIN") { - ra_tls::attestation::verify_pem(&cert, attestation_verifier.as_ref()).await + let cert_der = if cert.starts_with(b"-----BEGIN") { + let (_, pem) = + x509_parser::pem::parse_x509_pem(&cert).context("failed to parse PEM certificate")?; + pem.contents } else { - ra_tls::attestation::verify_der(&cert, attestation_verifier.as_ref()).await - } - .map_err(|e| anyhow::anyhow!("failed to verify RA-TLS certificate: {:#}", e))?; + cert + }; + verify_certificate_profile(&cert_der)?; + + let attestation_verifier = Arc::new(AttestationVerifier::load(&config.attestation)?); + let verified = ra_tls::attestation::verify_der(&cert_der, attestation_verifier.as_ref()) + .await + .map_err(|e| anyhow::anyhow!("failed to verify RA-TLS certificate: {:#}", e))?; let app_info = verified.attestation.decode_app_info(false).ok(); // Bind the reported os_image_hash to the attested boot measurement. For