From 1a7e70e160f0df1325b7842eebbe0873b258259c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 29 Jul 2026 16:43:55 +0000 Subject: [PATCH 1/4] fix(verifier): validate configuration precedence --- dstack/dstack-attest/src/attestation.rs | 2 + dstack/dstack-types/src/lib.rs | 1 + dstack/verifier/src/main.rs | 127 ++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/dstack/dstack-attest/src/attestation.rs b/dstack/dstack-attest/src/attestation.rs index a3b5b323c..6d597d87f 100644 --- a/dstack/dstack-attest/src/attestation.rs +++ b/dstack/dstack-attest/src/attestation.rs @@ -33,6 +33,7 @@ use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; /// File paths for attestation trust anchors. Empty fields retain the vendor /// production roots. Paths are read by the verifier, never by the attester. #[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RootCaPaths { pub tdx: Option, pub gcp_tpm: Option, @@ -44,6 +45,7 @@ pub struct RootCaPaths { } #[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct AttestationVerifierConfig { #[serde(default)] pub insecure_allow_external_trust_anchors: bool, diff --git a/dstack/dstack-types/src/lib.rs b/dstack/dstack-types/src/lib.rs index 141ed3bb9..10df03667 100644 --- a/dstack/dstack-types/src/lib.rs +++ b/dstack/dstack-types/src/lib.rs @@ -905,6 +905,7 @@ pub struct SysConfig { } #[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct CollateralUrls { #[serde(default, skip_serializing_if = "Option::is_none")] pub pccs: Option, diff --git a/dstack/verifier/src/main.rs b/dstack/verifier/src/main.rs index c8758ac99..9196768ae 100644 --- a/dstack/verifier/src/main.rs +++ b/dstack/verifier/src/main.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; +use std::{net::IpAddr, path::Path, sync::Arc}; use anyhow::{Context, Result}; use clap::Parser; @@ -36,6 +36,7 @@ struct Cli { } #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct Config { pub address: String, pub port: u16, @@ -46,6 +47,51 @@ pub struct Config { pub image_download_timeout_secs: u64, } +impl Config { + fn validate(&self) -> Result<()> { + self.address + .parse::() + .with_context(|| format!("invalid verifier address: {}", self.address))?; + anyhow::ensure!(self.port != 0, "verifier port must not be zero"); + anyhow::ensure!( + !self.image_cache_dir.trim().is_empty(), + "image_cache_dir must not be empty" + ); + anyhow::ensure!( + self.image_download_timeout_secs > 0, + "image_download_timeout_secs must be greater than zero" + ); + anyhow::ensure!( + self.image_download_url.contains("{OS_IMAGE_HASH}"), + "image_download_url must contain {OS_IMAGE_HASH}" + ); + let probe_url = self + .image_download_url + .replace("{OS_IMAGE_HASH}", &"00".repeat(32)); + let parsed = reqwest::Url::parse(&probe_url).context("invalid image_download_url")?; + anyhow::ensure!( + matches!(parsed.scheme(), "http" | "https"), + "image_download_url must use http or https" + ); + Ok(()) + } +} + +fn config_figment(config_path: &Path) -> Figment { + Figment::from(rocket::Config::default()) + .merge(Toml::string(include_str!("../dstack-verifier.toml"))) + .merge(Toml::file(config_path)) + .merge(Env::prefixed("DSTACK_VERIFIER_").split("__")) +} + +fn load_config(config_path: &Path) -> Result { + let config: Config = config_figment(config_path) + .extract() + .context("Failed to load configuration")?; + config.validate()?; + Ok(config) +} + #[post("/verify", data = "")] async fn verify_cvm( verifier: &State>, @@ -267,14 +313,9 @@ async fn main() -> Result<()> { let cli = Cli::parse(); - let default_config_str = include_str!("../dstack-verifier.toml"); - - let figment = Figment::from(rocket::Config::default()) - .merge(Toml::string(default_config_str)) - .merge(Toml::file(&cli.config)) - .merge(Env::prefixed("DSTACK_VERIFIER_")); - - let config: Config = figment.extract().context("Failed to load configuration")?; + let config_path = Path::new(&cli.config); + let figment = config_figment(config_path); + let config = load_config(config_path)?; // Check for oneshot modes if let Some(file_path) = cli.verify { if let Err(e) = run_oneshot(&file_path, &config).await { @@ -311,3 +352,71 @@ async fn main() -> Result<()> { .map_err(|err| anyhow::anyhow!("launch rocket failed: {err:?}"))?; Ok(()) } + +#[cfg(test)] +mod config_tests { + use super::*; + + fn valid_config(extra: &str) -> String { + format!( + r#"address = "127.0.0.1" +port = 18080 +image_cache_dir = "/tmp/dstack-verifier-config-test" +image_download_url = "http://127.0.0.1:18081/{{OS_IMAGE_HASH}}.tar.gz" +image_download_timeout_secs = 7 +{extra} +"# + ) + } + + #[test] + fn config_file_precedence_validation_and_unknown_field_matrix() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("verifier.toml"); + std::fs::write(&path, valid_config("")).unwrap(); + let loaded = load_config(&path).unwrap(); + assert_eq!(loaded.address, "127.0.0.1"); + assert_eq!(loaded.port, 18080); + assert_eq!(loaded.image_download_timeout_secs, 7); + + for (name, body, expected) in [ + ( + "zero-port", + valid_config("").replace("port = 18080", "port = 0"), + "port must not be zero", + ), + ( + "empty-cache", + valid_config("").replace("/tmp/dstack-verifier-config-test", ""), + "image_cache_dir must not be empty", + ), + ( + "zero-timeout", + valid_config("").replace("timeout_secs = 7", "timeout_secs = 0"), + "must be greater than zero", + ), + ( + "missing-placeholder", + valid_config("").replace("/{OS_IMAGE_HASH}.tar.gz", "/image.tar.gz"), + "must contain {OS_IMAGE_HASH}", + ), + ( + "invalid-scheme", + valid_config("").replace("http://", "file://"), + "must use http or https", + ), + ( + "unknown-field", + valid_config("unknown_policy = true"), + "unknown field", + ), + ] { + std::fs::write(&path, body).unwrap(); + let error = load_config(&path).expect_err(name); + assert!(format!("{error:#}").contains(expected), "{name}: {error:#}"); + } + + std::fs::write(&path, valid_config("")).unwrap(); + assert_eq!(load_config(&path).unwrap().port, 18080); + } +} From 838c245683d823b3d6d60edb924974d0bd315e86 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 29 Jul 2026 16:44:31 +0000 Subject: [PATCH 2/4] fix(verifier): escape image template diagnostic --- dstack/verifier/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstack/verifier/src/main.rs b/dstack/verifier/src/main.rs index 9196768ae..298fe4723 100644 --- a/dstack/verifier/src/main.rs +++ b/dstack/verifier/src/main.rs @@ -63,7 +63,7 @@ impl Config { ); anyhow::ensure!( self.image_download_url.contains("{OS_IMAGE_HASH}"), - "image_download_url must contain {OS_IMAGE_HASH}" + "image_download_url must contain {{OS_IMAGE_HASH}}" ); let probe_url = self .image_download_url From 969f15aba3d497376b8be6e864cad1a8a865184d Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 4 Aug 2026 20:15:04 -0700 Subject: [PATCH 3/4] fix(verifier): isolate app config extraction --- dstack/verifier/src/main.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/dstack/verifier/src/main.rs b/dstack/verifier/src/main.rs index 298fe4723..fa86bdb0e 100644 --- a/dstack/verifier/src/main.rs +++ b/dstack/verifier/src/main.rs @@ -65,14 +65,16 @@ impl Config { self.image_download_url.contains("{OS_IMAGE_HASH}"), "image_download_url must contain {{OS_IMAGE_HASH}}" ); + anyhow::ensure!( + self.image_download_url.starts_with("http://") + || self.image_download_url.starts_with("https://"), + "image_download_url must use http or https" + ); let probe_url = self .image_download_url .replace("{OS_IMAGE_HASH}", &"00".repeat(32)); let parsed = reqwest::Url::parse(&probe_url).context("invalid image_download_url")?; - anyhow::ensure!( - matches!(parsed.scheme(), "http" | "https"), - "image_download_url must use http or https" - ); + debug_assert!(matches!(parsed.scheme(), "http" | "https")); Ok(()) } } @@ -85,7 +87,13 @@ fn config_figment(config_path: &Path) -> Figment { } fn load_config(config_path: &Path) -> Result { - let config: Config = config_figment(config_path) + // Extract the verifier settings without Rocket's defaults. `Config` denies + // unknown fields, while Rocket's figment contains framework-only keys such + // as `cli_colors` that are still needed when launching the server. + let config: Config = Figment::new() + .merge(Toml::string(include_str!("../dstack-verifier.toml"))) + .merge(Toml::file(config_path)) + .merge(Env::prefixed("DSTACK_VERIFIER_").split("__")) .extract() .context("Failed to load configuration")?; config.validate()?; From f57f942269a24f40716a72c74ec5d8163404ec7d Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 4 Aug 2026 22:23:21 -0700 Subject: [PATCH 4/4] refactor(verifier): reuse config figment --- dstack/verifier/src/main.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/dstack/verifier/src/main.rs b/dstack/verifier/src/main.rs index fa86bdb0e..72861a742 100644 --- a/dstack/verifier/src/main.rs +++ b/dstack/verifier/src/main.rs @@ -80,22 +80,14 @@ impl Config { } fn config_figment(config_path: &Path) -> Figment { - Figment::from(rocket::Config::default()) + Figment::new() .merge(Toml::string(include_str!("../dstack-verifier.toml"))) .merge(Toml::file(config_path)) .merge(Env::prefixed("DSTACK_VERIFIER_").split("__")) } -fn load_config(config_path: &Path) -> Result { - // Extract the verifier settings without Rocket's defaults. `Config` denies - // unknown fields, while Rocket's figment contains framework-only keys such - // as `cli_colors` that are still needed when launching the server. - let config: Config = Figment::new() - .merge(Toml::string(include_str!("../dstack-verifier.toml"))) - .merge(Toml::file(config_path)) - .merge(Env::prefixed("DSTACK_VERIFIER_").split("__")) - .extract() - .context("Failed to load configuration")?; +fn load_config(figment: &Figment) -> Result { + let config: Config = figment.extract().context("Failed to load configuration")?; config.validate()?; Ok(config) } @@ -322,8 +314,8 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let config_path = Path::new(&cli.config); - let figment = config_figment(config_path); - let config = load_config(config_path)?; + let config_figment = config_figment(config_path); + let config = load_config(&config_figment)?; // Check for oneshot modes if let Some(file_path) = cli.verify { if let Err(e) = run_oneshot(&file_path, &config).await { @@ -347,7 +339,8 @@ async fn main() -> Result<()> { Arc::new(AttestationVerifier::load(&config.attestation)?), )); - rocket::custom(figment) + let rocket_figment = Figment::from(rocket::Config::default()).merge(config_figment); + rocket::custom(rocket_figment) .mount("/", rocket::routes![verify_cvm, health]) .manage(verifier) .attach(AdHoc::on_liftoff("Startup", |_| { @@ -382,7 +375,7 @@ image_download_timeout_secs = 7 let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("verifier.toml"); std::fs::write(&path, valid_config("")).unwrap(); - let loaded = load_config(&path).unwrap(); + let loaded = load_config(&config_figment(&path)).unwrap(); assert_eq!(loaded.address, "127.0.0.1"); assert_eq!(loaded.port, 18080); assert_eq!(loaded.image_download_timeout_secs, 7); @@ -420,11 +413,11 @@ image_download_timeout_secs = 7 ), ] { std::fs::write(&path, body).unwrap(); - let error = load_config(&path).expect_err(name); + let error = load_config(&config_figment(&path)).expect_err(name); assert!(format!("{error:#}").contains(expected), "{name}: {error:#}"); } std::fs::write(&path, valid_config("")).unwrap(); - assert_eq!(load_config(&path).unwrap().port, 18080); + assert_eq!(load_config(&config_figment(&path)).unwrap().port, 18080); } }