Skip to content
Merged
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
2 changes: 2 additions & 0 deletions dstack/dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::path::PathBuf>,
pub gcp_tpm: Option<std::path::PathBuf>,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions dstack/dstack-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand Down
130 changes: 120 additions & 10 deletions dstack/verifier/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,6 +36,7 @@ struct Cli {
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub address: String,
pub port: u16,
Expand All @@ -46,6 +47,51 @@ pub struct Config {
pub image_download_timeout_secs: u64,
}

impl Config {
fn validate(&self) -> Result<()> {
self.address
.parse::<IpAddr>()
.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}}"
);
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")?;
debug_assert!(matches!(parsed.scheme(), "http" | "https"));
Ok(())
}
}

fn config_figment(config_path: &Path) -> Figment {
Figment::new()
.merge(Toml::string(include_str!("../dstack-verifier.toml")))
.merge(Toml::file(config_path))
.merge(Env::prefixed("DSTACK_VERIFIER_").split("__"))
}

fn load_config(figment: &Figment) -> Result<Config> {
let config: Config = figment.extract().context("Failed to load configuration")?;
config.validate()?;
Ok(config)
}

#[post("/verify", data = "<request>")]
async fn verify_cvm(
verifier: &State<Arc<CvmVerifier>>,
Expand Down Expand Up @@ -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 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 {
Expand All @@ -298,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", |_| {
Expand All @@ -311,3 +353,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(&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);

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(&config_figment(&path)).expect_err(name);
assert!(format!("{error:#}").contains(expected), "{name}: {error:#}");
}

std::fs::write(&path, valid_config("")).unwrap();
assert_eq!(load_config(&config_figment(&path)).unwrap().port, 18080);
}
}
Loading