diff --git a/Cargo.lock b/Cargo.lock index a17f60274..146e8670c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2182,6 +2182,7 @@ dependencies = [ "cc-eventlog", "dcap-qvl", "dstack-types", + "errify", "ez-hash", "fs-err", "futures", @@ -2495,6 +2496,7 @@ dependencies = [ "ra-tls", "rand 0.8.5", "regex", + "safe-write", "schnorrkel", "scopeguard", "serde", @@ -2782,6 +2784,28 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errify" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb818c3c01af9cdeb367f7e92e290b9a080935cdc5fb6cc0c1193ae17032849" +dependencies = [ + "anyhow", + "errify-macros", +] + +[[package]] +name = "errify-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e87afa19e6030c2cf5514b00d5a242a3ea9492a2aa618635076914f5d15e7af" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.111", +] + [[package]] name = "errno" version = "0.3.14" @@ -5633,6 +5657,7 @@ dependencies = [ "dstack-attest", "dstack-types", "elliptic-curve", + "errify", "ez-hash", "flate2", "fs-err", @@ -5645,6 +5670,7 @@ dependencies = [ "rand 0.8.5", "rcgen", "ring", + "rmp-serde", "rustls-pki-types", "serde", "serde-human-bytes", @@ -6003,6 +6029,25 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "rocket" version = "0.6.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 0758da533..446954ba1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ size-parser = { path = "size-parser" } # Core dependencies anyhow = { version = "1.0.97", default-features = false } +errify = { version = "0.3.0", features = ["anyhow"] } or-panic = { version = "1.0", default-features = false } chrono = "0.4.40" clap = { version = "4.5.32", features = ["derive", "string"] } @@ -124,6 +125,7 @@ serde = { version = "1.0.228", features = ["derive"], default-features = false } serde-human-bytes = "0.1.2" serde_json = { version = "1.0.140", default-features = false } serde_ini = "0.2.0" +rmp-serde = "1.3.1" toml = "0.8.20" toml_edit = { version = "0.22.24", features = ["serde"] } yasna = "0.5.2" diff --git a/basefiles/wg-checker.service b/basefiles/wg-checker.service index d6009b540..406cadc69 100644 --- a/basefiles/wg-checker.service +++ b/basefiles/wg-checker.service @@ -8,7 +8,7 @@ Type=simple ExecStart=/bin/wg-checker.sh Restart=always RestartSec=10 -StandardOutput=journal+console +StandardOutput=journal StandardError=journal+console [Install] diff --git a/basefiles/wg-checker.sh b/basefiles/wg-checker.sh index d97635280..ba4149472 100755 --- a/basefiles/wg-checker.sh +++ b/basefiles/wg-checker.sh @@ -5,6 +5,7 @@ # SPDX-License-Identifier: Apache-2.0 HANDSHAKE_TIMEOUT=180 +REFRESH_INTERVAL=180 LAST_REFRESH=0 STALE_SINCE=0 DSTACK_WORK_DIR=${DSTACK_WORK_DIR:-/dstack} @@ -14,12 +15,10 @@ get_latest_handshake() { wg show $IFNAME latest-handshakes 2>/dev/null | awk 'BEGIN { max = 0 } NF >= 2 { if ($2 > max) max = $2 } END { print max }' } -maybe_refresh() { +do_refresh() { now=$1 - - if [ "$LAST_REFRESH" -ne 0 ] && [ $((now - LAST_REFRESH)) -lt $HANDSHAKE_TIMEOUT ]; then - return - fi + reason=$2 + force=$3 if ! command -v dstack-util >/dev/null 2>&1; then printf 'dstack-util not found; cannot refresh gateway.\n' >&2 @@ -27,48 +26,58 @@ maybe_refresh() { return fi - printf 'WireGuard handshake stale; refreshing dstack gateway...\n' - if dstack-util gateway-refresh --work-dir "$DSTACK_WORK_DIR"; then + printf '%s; refreshing dstack gateway...\n' "$reason" + if [ "$force" = "1" ]; then + cmd="dstack-util gateway-refresh --work-dir $DSTACK_WORK_DIR --force" + else + cmd="dstack-util gateway-refresh --work-dir $DSTACK_WORK_DIR" + fi + if $cmd; then printf 'dstack gateway refresh succeeded.\n' else printf 'dstack gateway refresh failed.\n' >&2 fi LAST_REFRESH=$now - STALE_SINCE=$now + STALE_SINCE=0 } -check_handshake() { +check_and_refresh() { if ! command -v wg >/dev/null 2>&1; then return fi now=$(date +%s) - latest=$(get_latest_handshake) + # Periodic refresh every REFRESH_INTERVAL seconds (not forced) + if [ "$LAST_REFRESH" -eq 0 ] || [ $((now - LAST_REFRESH)) -ge $REFRESH_INTERVAL ]; then + do_refresh "$now" "Periodic refresh" 0 + return + fi + + # Check handshake staleness (forced refresh) + latest=$(get_latest_handshake) if [ -z "$latest" ]; then latest=0 fi if [ "$latest" -gt 0 ]; then if [ $((now - latest)) -ge $HANDSHAKE_TIMEOUT ]; then - maybe_refresh "$now" - else - STALE_SINCE=0 + do_refresh "$now" "WireGuard handshake stale" 1 >&2 fi else if [ "$STALE_SINCE" -eq 0 ]; then STALE_SINCE=$now fi if [ $((now - STALE_SINCE)) -ge $HANDSHAKE_TIMEOUT ]; then - maybe_refresh "$now" + do_refresh "$now" "WireGuard handshake stale" 1 >&2 fi fi } while true; do if [ -f /etc/wireguard/$IFNAME.conf ]; then - check_handshake + check_and_refresh else STALE_SINCE=0 fi diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml index 13f8e7b2a..77834d1c5 100644 --- a/dstack-attest/Cargo.toml +++ b/dstack-attest/Cargo.toml @@ -27,6 +27,7 @@ sha2.workspace = true sha3.workspace = true tdx-attest.workspace = true insta.workspace = true +errify.workspace = true [features] quote = [] diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index 4e841583d..6250c2dd8 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -260,6 +260,13 @@ impl VersionedAttestation { } } + /// Get app info + pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + match self { + Self::V0 { attestation } => attestation.decode_app_info(boottime_mr), + } + } + /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). pub fn into_stripped(mut self) -> Self { let VersionedAttestation::V0 { attestation } = &mut self; @@ -444,6 +451,7 @@ impl Attestation { self.decode_app_info_ex(boottime_mr, "") } + #[errify::errify("decode app info")] pub fn decode_app_info_ex(&self, boottime_mr: bool, vm_config: &str) -> Result { let key_provider_info = if boottime_mr { vec![] @@ -739,7 +747,7 @@ pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> { } /// Information about the app extracted from event log -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppInfo { /// App ID #[serde(with = "hex_bytes")] diff --git a/dstack-util/Cargo.toml b/dstack-util/Cargo.toml index e8b47a90e..7a64a6484 100644 --- a/dstack-util/Cargo.toml +++ b/dstack-util/Cargo.toml @@ -55,6 +55,7 @@ scopeguard.workspace = true tempfile.workspace = true ez-hash.workspace = true cc-eventlog.workspace = true +safe-write.workspace = true [dev-dependencies] rand.workspace = true diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 91f1ea66d..ce1af40d2 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -30,6 +30,7 @@ use luks2::{ use ra_rpc::client::{CertInfo, RaClient, RaClientConfig}; use ra_tls::cert::{generate_ra_cert, CertConfigV2}; use rand::Rng as _; +use safe_write::safe_write; use scopeguard::defer; use serde::{Deserialize, Serialize}; use tracing::{info, warn}; @@ -74,6 +75,9 @@ pub struct GatewayRefreshArgs { /// dstack work directory #[arg(long)] work_dir: PathBuf, + /// Force reconfiguration even if config unchanged + #[arg(long)] + force: bool, } #[derive(Deserialize, Serialize, Clone, Default)] @@ -327,6 +331,47 @@ impl HostShared { } } +const GATEWAY_CACHE_PATH: &str = "/run/dstack/gateway-cache.json"; +const WG_CONFIG_PATH: &str = "/etc/wireguard/dstack-wg0.conf"; +/// Certificate validity period in seconds (10 days) +const CERT_VALIDITY_SECS: u64 = 10 * 24 * 3600; + +#[derive(Serialize, Deserialize)] +struct GatewayCache { + /// Client certificate chain + client_cert: String, + /// Client private key + client_key: String, + /// Certificate expiry time as seconds since UNIX epoch + cert_not_after: u64, + /// WireGuard private key + wg_sk: String, + /// WireGuard public key + wg_pk: String, +} + +impl GatewayCache { + fn load() -> Option { + let content = fs::read_to_string(GATEWAY_CACHE_PATH).ok()?; + serde_json::from_str(&content).ok() + } + + fn save(&self) -> Result<()> { + let content = serde_json::to_string(self).context("Failed to serialize gateway cache")?; + safe_write(GATEWAY_CACHE_PATH, &content).context("Failed to write gateway cache")?; + Ok(()) + } + + fn is_cert_valid(&self) -> bool { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + // Valid if at least 10 minutes remaining + now + 600 < self.cert_not_after + } +} + struct GatewayContext<'a> { shared: &'a HostShared, keys: &'a AppKeys, @@ -371,29 +416,44 @@ impl<'a> GatewayContext<'a> { .context("Failed to register CVM") } - async fn setup(&self) -> Result<()> { - if !self.shared.app_compose.gateway_enabled() { - info!("dstack-gateway is not enabled"); - return Ok(()); - } - if self.keys.gateway_app_id.is_empty() { - bail!("Missing allowed dstack-gateway app id"); + fn get_or_generate_wg_keys(cache: Option<&GatewayCache>) -> Result<(String, String)> { + if let Some(cache) = cache { + info!("Using cached WireGuard keys"); + return Ok((cache.wg_sk.clone(), cache.wg_pk.clone())); } - info!("Setting up dstack-gateway"); - // Generate WireGuard keys + info!("Generating new WireGuard keys"); let sk = cmd!(wg genkey)?; let pk = cmd!(echo $sk | wg pubkey).or(Err(anyhow!("Failed to generate public key")))?; + Ok((sk, pk)) + } + + async fn get_or_request_client_cert( + &self, + cache: Option<&GatewayCache>, + ) -> Result<(String, String)> { + if let Some(cache) = cache.filter(|c| c.is_cert_valid()) { + info!("Using cached client certificate"); + return Ok((cache.client_cert.clone(), cache.client_key.clone())); + } + info!("Requesting new client certificate"); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let not_after = now + CERT_VALIDITY_SECS; let config = CertConfigV2 { org_name: None, subject: "dstack-guest-agent".to_string(), subject_alt_names: vec![], usage_server_auth: false, usage_client_auth: true, + // TODO: Disable this once pre-0.5.6 gateways are deprecated ext_quote: true, + ext_app_info: true, not_before: None, - not_after: None, + not_after: Some(not_after), }; let cert_client = CertRequestClient::create( self.keys, @@ -402,14 +462,50 @@ impl<'a> GatewayContext<'a> { ) .await .context("Failed to create cert client")?; - let client_key = + let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).context("Failed to generate key")?; - let client_certs = cert_client - .request_cert(&client_key, config, None) + let certs = cert_client + .request_cert(&key, config, None) .await .context("Failed to request cert")?; - let client_cert = client_certs.join("\n"); - let client_key = client_key.serialize_pem(); + let cert = certs.join("\n"); + let key = key.serialize_pem(); + + Ok((cert, key)) + } + + async fn setup(&self, force: bool) -> Result<()> { + if !self.shared.app_compose.gateway_enabled() { + info!("dstack-gateway is not enabled"); + return Ok(()); + } + if self.keys.gateway_app_id.is_empty() { + bail!("Missing allowed dstack-gateway app id"); + } + + info!("Setting up dstack-gateway"); + + // Load cached data if available + let cache = GatewayCache::load(); + + // Get or generate WireGuard keys + let (wg_sk, wg_pk) = Self::get_or_generate_wg_keys(cache.as_ref())?; + + // Get or request client certificate + let (client_cert, client_key) = self.get_or_request_client_cert(cache.as_ref()).await?; + + // Calculate cert expiry for cache + let cert_not_after = cache + .as_ref() + .filter(|c| c.is_cert_valid()) + .map(|c| c.cert_not_after) + .unwrap_or_else(|| { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + now + CERT_VALIDITY_SECS + }); if self.shared.sys_config.gateway_urls.is_empty() { bail!("Missing gateway urls"); @@ -419,7 +515,7 @@ impl<'a> GatewayContext<'a> { let mut error = anyhow!("unknown error"); for (i, url) in self.shared.sys_config.gateway_urls.iter().enumerate() { let response = self - .register_cvm(url, client_key.clone(), client_cert.clone(), pk.clone()) + .register_cvm(url, client_key.clone(), client_cert.clone(), wg_pk.clone()) .await; match response { Ok(response) => { @@ -435,21 +531,24 @@ impl<'a> GatewayContext<'a> { } return Err(error).context("Failed to register CVM, all dstack-gateway urls are down"); }; - let wg_info = response.wg.context("Missing wg info")?; + let mut wg_info = response.wg.context("Missing wg info")?; let client_ip = &wg_info.client_ip; + // Sort peers by public key for consistent config generation + wg_info.servers.sort_by(|a, b| a.pk.cmp(&b.pk)); + // Create WireGuard config let wg_listen_port = "9182"; - let mut config = format!( + let mut new_config = format!( "[Interface]\n\ - PrivateKey = {sk}\n\ + PrivateKey = {wg_sk}\n\ ListenPort = {wg_listen_port}\n\ Address = {client_ip}/32\n\n" ); for WireGuardPeer { pk, ip, endpoint } in &wg_info.servers { let ip = ip.split('/').next().unwrap_or_default(); - config.push_str(&format!( + new_config.push_str(&format!( "[Peer]\n\ PublicKey = {pk}\n\ AllowedIPs = {ip}/32\n\ @@ -458,9 +557,30 @@ impl<'a> GatewayContext<'a> { )); } + // Save cache + let new_cache = GatewayCache { + client_cert, + client_key, + cert_not_after, + wg_sk, + wg_pk, + }; + if let Err(e) = new_cache.save() { + warn!("Failed to save gateway cache: {e:?}"); + } + + // Check if config has changed (skip check if force is set) + if !force { + let current_config = fs::read_to_string(WG_CONFIG_PATH).ok(); + if current_config.as_ref() == Some(&new_config) { + info!("WireGuard config unchanged, skipping reconfiguration"); + return Ok(()); + } + } + let wg_dir = Path::new("/etc/wireguard"); fs::create_dir_all(wg_dir)?; - fs::write(wg_dir.join("dstack-wg0.conf"), config)?; + fs::write(wg_dir.join("dstack-wg0.conf"), &new_config)?; cmd! { chmod 600 $wg_dir/dstack-wg0.conf; @@ -549,7 +669,7 @@ pub async fn cmd_gateway_refresh(args: GatewayRefreshArgs) -> Result<()> { let keys: AppKeys = deserialize_json_file(&keys_path) .with_context(|| format!("Failed to load app keys from {}", keys_path.display()))?; - GatewayContext::new(&shared, &keys).setup().await + GatewayContext::new(&shared, &keys).setup(args.force).await } struct AppIdValidator { @@ -1289,7 +1409,7 @@ impl Stage1<'_> { .notify_q("boot.progress", "setting up dstack-gateway") .await; GatewayContext::new(&self.shared, &self.keys) - .setup() + .setup(true) .await?; self.vmm .notify_q("boot.progress", "setting up docker") diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 17ef2cf37..df8b3240e 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -77,6 +77,7 @@ async fn maybe_gen_certs(config: &Config, tls_config: &TlsConfig) -> Result<()> usage_ra_tls: true, usage_server_auth: true, usage_client_auth: false, + with_app_info: false, not_before: None, not_after: None, }) diff --git a/gateway/src/main_service.rs b/gateway/src/main_service.rs index e6bc67754..7f211d302 100644 --- a/gateway/src/main_service.rs +++ b/gateway/src/main_service.rs @@ -25,7 +25,7 @@ use fs_err as fs; use http_client::prpc::PrpcClient; use or_panic::ResultOrPanic; use ra_rpc::{CallContext, RpcCall, VerifiedAttestation}; -use ra_tls::attestation::QuoteContentType; +use ra_tls::attestation::{AppInfo, QuoteContentType}; use rand::seq::IteratorRandom; use rinja::Template as _; use safe_write::safe_write; @@ -709,6 +709,7 @@ pub(crate) fn encode_ts(ts: SystemTime) -> u64 { pub struct RpcHandler { remote_app_id: Option>, + remote_app_info: Option, attestation: Option, state: Proxy, } @@ -730,12 +731,16 @@ impl RpcHandler { impl GatewayRpc for RpcHandler { async fn register_cvm(self, request: RegisterCvmRequest) -> Result { - let Some(ra) = &self.attestation else { - bail!("no attestation provided"); + let app_info = match self.remote_app_info { + Some(app_info) => app_info, + None => { + let Some(ra) = &self.attestation else { + bail!("neither app-info nor attestation provided"); + }; + ra.decode_app_info(false) + .context("failed to decode app-info from attestation")? + } }; - let app_info = ra - .decode_app_info(false) - .context("failed to decode app-info from attestation")?; self.state .auth_client .ensure_app_authorized(&app_info) @@ -869,6 +874,7 @@ impl RpcCall for RpcHandler { fn construct(context: CallContext<'_, Proxy>) -> Result { Ok(RpcHandler { remote_app_id: context.remote_app_id, + remote_app_info: context.remote_app_info, attestation: context.attestation, state: context.state.clone(), }) diff --git a/gateway/src/main_service/sync_client.rs b/gateway/src/main_service/sync_client.rs index be6985f78..9763175d4 100644 --- a/gateway/src/main_service/sync_client.rs +++ b/gateway/src/main_service/sync_client.rs @@ -90,6 +90,7 @@ pub(crate) async fn sync_task(proxy: Proxy) -> Result<()> { usage_ra_tls: false, usage_server_auth: false, usage_client_auth: true, + with_app_info: false, not_after: None, not_before: None, }) diff --git a/guest-agent/rpc/proto/agent_rpc.proto b/guest-agent/rpc/proto/agent_rpc.proto index 86bec3573..2036fb440 100644 --- a/guest-agent/rpc/proto/agent_rpc.proto +++ b/guest-agent/rpc/proto/agent_rpc.proto @@ -76,6 +76,8 @@ message GetTlsKeyArgs { optional uint64 not_before = 6; // Certificate validity end time as seconds since UNIX epoch optional uint64 not_after = 7; + // Includes app info in the certificate + bool with_app_info = 8; } // The request to derive a key diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 8e03efaa4..5d88fd2c9 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -85,6 +85,7 @@ impl AppStateInner { usage_server_auth: false, usage_client_auth: true, ext_quote: true, + ext_app_info: false, not_after: None, not_before: None, }, @@ -242,6 +243,7 @@ impl DstackGuestRpc for InternalRpcHandler { usage_server_auth: request.usage_server_auth, usage_client_auth: request.usage_client_auth, ext_quote: request.usage_ra_tls, + ext_app_info: request.with_app_info, not_after: request.not_after, not_before: request.not_before, }; @@ -504,6 +506,7 @@ impl TappdRpc for InternalRpcHandlerV0 { usage_server_auth: request.usage_server_auth, usage_client_auth: request.usage_client_auth, ext_quote: request.usage_ra_tls, + ext_app_info: false, not_before: None, not_after: None, }; diff --git a/ra-rpc/src/lib.rs b/ra-rpc/src/lib.rs index 6dc54b983..253d8eaa7 100644 --- a/ra-rpc/src/lib.rs +++ b/ra-rpc/src/lib.rs @@ -8,6 +8,7 @@ use std::{fmt::Display, net::SocketAddr, path::PathBuf}; use anyhow::Result; use prpc::{codec::encode_message_to_vec, server::Service as PrpcService}; +use ra_tls::attestation::AppInfo; use tracing::{error, info}; pub use ra_tls::attestation::{Attestation, VerifiedAttestation}; @@ -36,6 +37,7 @@ pub struct CallContext<'a, State> { pub attestation: Option, pub remote_endpoint: Option, pub remote_app_id: Option>, + pub remote_app_info: Option, } pub trait RpcCall: Sized { diff --git a/ra-rpc/src/rocket_helper.rs b/ra-rpc/src/rocket_helper.rs index 8cc712b26..770b07399 100644 --- a/ra-rpc/src/rocket_helper.rs +++ b/ra-rpc/src/rocket_helper.rs @@ -291,12 +291,19 @@ pub async fn handle_prpc_impl>( data, } = args; let method = method.trim_start_matches(method_trim_prefix.unwrap_or_default()); - let remote_app_id = request + let info = request .certificate .as_ref() - .map(|cert| RocketCertificate(cert).get_app_id()) - .transpose()? - .flatten(); + .map(|cert| -> Result<_> { + let app_id = RocketCertificate(cert).get_app_id()?; + let app_info = RocketCertificate(cert).get_app_info()?; + Ok((app_id, app_info)) + }) + .transpose()?; + let (remote_app_id, remote_app_info) = match info { + Some((app_id, app_info)) => (app_id, app_info), + None => (None, None), + }; let attestation = request .certificate .as_ref() @@ -337,6 +344,7 @@ pub async fn handle_prpc_impl>( attestation, remote_endpoint: request.remote_addr.cloned().map(RemoteEndpoint::from), remote_app_id, + remote_app_info, }; let call = Call::construct(context).context("failed to construct call")?; let (status_code, output) = call diff --git a/ra-tls/Cargo.toml b/ra-tls/Cargo.toml index 99622298c..ab1ca9b20 100644 --- a/ra-tls/Cargo.toml +++ b/ra-tls/Cargo.toml @@ -39,6 +39,8 @@ dstack-types.workspace = true hex_fmt.workspace = true ez-hash.workspace = true dstack-attest.workspace = true +errify.workspace = true +rmp-serde.workspace = true [features] quote = ["dstack-attest/quote"] diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index 27fb436fd..793fa1a7d 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -24,13 +24,13 @@ use x509_parser::public_key::PublicKey; use x509_parser::x509::SubjectPublicKeyInfo; use crate::oids::{ - PHALA_RATLS_APP_ID, PHALA_RATLS_ATTESTATION, PHALA_RATLS_CERT_USAGE, PHALA_RATLS_EVENT_LOG, - PHALA_RATLS_TDX_QUOTE, + PHALA_RATLS_APP_ID, PHALA_RATLS_APP_INFO, PHALA_RATLS_ATTESTATION, PHALA_RATLS_CERT_USAGE, + PHALA_RATLS_EVENT_LOG, PHALA_RATLS_TDX_QUOTE, }; use crate::traits::CertExt; #[cfg(feature = "quote")] use dstack_attest::attestation::QuoteContentType; -use dstack_attest::attestation::{Attestation, AttestationQuote, VersionedAttestation}; +use dstack_attest::attestation::{AppInfo, Attestation, AttestationQuote, VersionedAttestation}; /// A CA certificate and private key. pub struct CaCert { @@ -88,6 +88,11 @@ impl CaCert { let pki = rcgen::SubjectPublicKeyInfo::from_der(&csr.pubkey) .context("Failed to parse signature")?; let cfg = &csr.config; + let app_info = if cfg.ext_app_info { + Some(csr.attestation.decode_app_info(false)?) + } else { + None + }; let attestation = cfg.ext_quote.then_some(&csr.attestation); let req = CertRequest::builder() .key(&pki) @@ -98,6 +103,7 @@ impl CaCert { .usage_client_auth(cfg.usage_client_auth) .maybe_attestation(attestation) .maybe_app_id(app_id) + .maybe_app_info(app_info.as_ref()) .special_usage(usage) .maybe_not_before(cfg.not_before.map(unix_time_to_system_time)) .maybe_not_after(cfg.not_after.map(unix_time_to_system_time)) @@ -138,6 +144,8 @@ pub struct CertConfigV2 { pub usage_client_auth: bool, /// Whether the certificate is quoted. pub ext_quote: bool, + /// Whether embed app info. + pub ext_app_info: bool, /// The certificate validity start time as seconds since UNIX epoch. pub not_before: Option, /// The certificate validity end time as seconds since UNIX epoch. @@ -153,6 +161,7 @@ impl From for CertConfigV2 { usage_server_auth: config.usage_server_auth, usage_client_auth: config.usage_client_auth, ext_quote: config.ext_quote, + ext_app_info: false, not_before: None, not_after: None, } @@ -330,6 +339,7 @@ pub struct CertRequest<'a, Key> { alt_names: Option<&'a [String]>, ca_level: Option, app_id: Option<&'a [u8]>, + app_info: Option<&'a AppInfo>, special_usage: Option<&'a str>, attestation: Option<&'a VersionedAttestation>, not_before: Option, @@ -370,6 +380,11 @@ impl CertRequest<'_, Key> { if let Some(app_id) = self.app_id { add_ext(&mut params, PHALA_RATLS_APP_ID, app_id); } + if let Some(app_info) = self.app_info { + let app_info_bytes = + rmp_serde::to_vec(&app_info).context("Failed to serialize app info")?; + add_ext(&mut params, PHALA_RATLS_APP_INFO, app_info_bytes); + } if let Some(usage) = self.special_usage { add_ext(&mut params, PHALA_RATLS_CERT_USAGE, usage); } @@ -673,6 +688,7 @@ mod tests { usage_server_auth: true, usage_client_auth: false, ext_quote: false, + ext_app_info: false, not_before: None, not_after: None, }, @@ -706,6 +722,7 @@ mod tests { usage_server_auth: true, usage_client_auth: false, ext_quote: true, + ext_app_info: false, not_before: None, not_after: None, }, diff --git a/ra-tls/src/oids.rs b/ra-tls/src/oids.rs index 38e71d1bc..3535ac1fb 100644 --- a/ra-tls/src/oids.rs +++ b/ra-tls/src/oids.rs @@ -14,3 +14,5 @@ pub const PHALA_RATLS_APP_ID: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 3]; pub const PHALA_RATLS_CERT_USAGE: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 4]; /// OID for dstack attestation (the successor of TDX quote) pub const PHALA_RATLS_ATTESTATION: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 8]; +/// OID for app info +pub const PHALA_RATLS_APP_INFO: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 9]; diff --git a/ra-tls/src/traits.rs b/ra-tls/src/traits.rs index aaac91158..5cd8e7c58 100644 --- a/ra-tls/src/traits.rs +++ b/ra-tls/src/traits.rs @@ -5,14 +5,17 @@ //! Traits for the crate use anyhow::{Context, Result}; +use dstack_attest::attestation::AppInfo; -use crate::oids::{PHALA_RATLS_APP_ID, PHALA_RATLS_CERT_USAGE}; +use crate::oids::{PHALA_RATLS_APP_ID, PHALA_RATLS_APP_INFO, PHALA_RATLS_CERT_USAGE}; /// Types that can get custom cert extensions from. pub trait CertExt { /// Get a cert extension from the type. fn get_extension_der(&self, oid: &[u64]) -> Result>>; + /// Get externtion bytes + #[errify::errify("Failed to get extension for {oid:?}")] fn get_extension_bytes(&self, oid: &[u64]) -> Result>> { let Some(der) = self.get_extension_der(oid)? else { return Ok(None); @@ -23,10 +26,7 @@ pub trait CertExt { /// Get Certificate Special Usage from the type. fn get_special_usage(&self) -> Result> { - let Some(found) = self - .get_extension_bytes(PHALA_RATLS_CERT_USAGE) - .context("Failed to get extension")? - else { + let Some(found) = self.get_extension_bytes(PHALA_RATLS_CERT_USAGE)? else { return Ok(None); }; let found = String::from_utf8(found).context("Failed to decode special usage as utf8")?; @@ -35,9 +35,16 @@ pub trait CertExt { /// Get the app id from the certificate fn get_app_id(&self) -> Result>> { - let app_id = self - .get_extension_bytes(PHALA_RATLS_APP_ID) - .context("Failed to get extension")?; - Ok(app_id) + self.get_extension_bytes(PHALA_RATLS_APP_ID) + } + + /// Get app info + fn get_app_info(&self) -> Result> { + let Some(app_info_bytes) = self.get_extension_bytes(PHALA_RATLS_APP_INFO)? else { + return Ok(None); + }; + let app_info = + rmp_serde::from_slice(&app_info_bytes).context("Failed to decode app info as json")?; + Ok(app_info) } } diff --git a/sdk/curl/api.md b/sdk/curl/api.md index 32f80bca5..b05a6717b 100644 --- a/sdk/curl/api.md +++ b/sdk/curl/api.md @@ -33,6 +33,9 @@ Derives a cryptographic key and returns it along with its TLS certificate chain. | `usage_ra_tls` | boolean | Whether to include quote in the certificate for RA-TLS | `true` | | `usage_server_auth` | boolean | Enable certificate for server authentication | `true` | | `usage_client_auth` | boolean | Enable certificate for client authentication | `false` | +| `not_before` | uint64 | Certificate validity start time as seconds since UNIX epoch | `0` | +| `not_after` | uint64 | Certificate validity end time as seconds since UNIX epoch | `0` | +| `with_app_info` | boolean | Whether to include app info in the certificate | `false` | **Example:** ```bash