Skip to content

Commit b73ce76

Browse files
committed
feat(guest): route GPU attestation collateral through optional proxy
When sys-config carries gpu_attest_proxy_url, run nvattest with --ocsp-url/--rim-url pointing at the proxy and --relying-party-policy allow_trust_outpost_ocsp.rego so cached OCSP responses (which cannot match the request nonce) are still appraised for signature, validity window and measurements. Fail closed if the policy file is missing.
1 parent cc7be3b commit b73ce76

1 file changed

Lines changed: 96 additions & 22 deletions

File tree

dstack/dstack-util/src/system_setup.rs

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,10 @@ mod gpu {
10811081
const ATTESTATION_TIMEOUT: Duration = Duration::from_secs(300);
10821082
const EVENT_VERSION: u32 = 2;
10831083
const POLICY_ENTRYPOINT: &str = "data.policy.nv_match";
1084+
/// NVIDIA's sample relying-party policy packaged by the nvattest recipe.
1085+
/// It keeps every built-in appraisal check except the OCSP nonce match,
1086+
/// which a caching collateral proxy can never satisfy.
1087+
const OUTPOST_POLICY: &str = "/usr/share/nvattest/policies/allow_trust_outpost_ocsp.rego";
10841088

10851089
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10861090
pub(super) struct GpuInventory {
@@ -1366,11 +1370,70 @@ mod gpu {
13661370
serde_json::to_vec(&event).context("failed to serialize GPU attestation event")
13671371
}
13681372

1369-
/// Run local GPU attestation via nvattest with a fresh nonce and no custom
1370-
/// relying-party policy. nvattest's built-in appraisal still applies. The
1371-
/// complete JSON output is preserved under /run and retained with its
1372-
/// claims for the application policy and versioned RTMR3 summary.
1373-
pub(super) async fn attest_gpu(expected_devices: u32) -> Result<GpuAttestationResult> {
1373+
/// Build the nvattest command line. When `proxy_url` is set, OCSP and RIM
1374+
/// requests are routed through the caching collateral proxy and the
1375+
/// packaged outpost policy replaces nvattest's built-in appraisal (a
1376+
/// cached OCSP response can never echo the per-request nonce). The policy
1377+
/// file must exist — it is packaged by the nvattest recipe; fail closed
1378+
/// with a clear error rather than silently skipping appraisal.
1379+
fn nvattest_args(nonce: &str, proxy_url: Option<&str>) -> Result<Vec<String>> {
1380+
let proxy_base = match proxy_url {
1381+
Some(url) => {
1382+
let base = url.trim_end_matches('/');
1383+
if base.is_empty() {
1384+
bail!("sys-config gpu_attest_proxy_url is empty");
1385+
}
1386+
Some(base)
1387+
}
1388+
None => None,
1389+
};
1390+
if proxy_base.is_some() && !Path::new(OUTPOST_POLICY).exists() {
1391+
bail!(
1392+
"sys-config gpu_attest_proxy_url is set but {} is not packaged in this image",
1393+
OUTPOST_POLICY
1394+
);
1395+
}
1396+
Ok(nvattest_cmdline(nonce, proxy_base))
1397+
}
1398+
1399+
fn nvattest_cmdline(nonce: &str, proxy_base: Option<&str>) -> Vec<String> {
1400+
let mut args = vec![
1401+
"attest".to_string(),
1402+
"--device".to_string(),
1403+
"gpu".to_string(),
1404+
"--verifier".to_string(),
1405+
"local".to_string(),
1406+
"--nonce".to_string(),
1407+
nonce.to_string(),
1408+
"--format".to_string(),
1409+
"json".to_string(),
1410+
];
1411+
if let Some(base) = proxy_base.map(|url| url.trim_end_matches('/')) {
1412+
args.extend([
1413+
"--ocsp-url".to_string(),
1414+
format!("{base}/ocsp"),
1415+
"--rim-url".to_string(),
1416+
base.to_string(),
1417+
"--relying-party-policy".to_string(),
1418+
OUTPOST_POLICY.to_string(),
1419+
]);
1420+
}
1421+
args
1422+
}
1423+
1424+
/// Run local GPU attestation via nvattest with a fresh nonce. Without a
1425+
/// collateral proxy no custom relying-party policy is given, so nvattest's
1426+
/// built-in appraisal applies. With `proxy_url` (sys-config
1427+
/// `gpu_attest_proxy_url`), OCSP and RIM requests go through the caching
1428+
/// proxy and the packaged outpost policy replaces the built-in one: a
1429+
/// cached OCSP response can never echo the request nonce, while signature,
1430+
/// validity-window and measurement checks are unchanged. The complete JSON
1431+
/// output is preserved under /run and retained with its claims for the
1432+
/// application policy and versioned RTMR3 summary.
1433+
pub(super) async fn attest_gpu(
1434+
expected_devices: u32,
1435+
proxy_url: Option<&str>,
1436+
) -> Result<GpuAttestationResult> {
13741437
if !Path::new(NVATTEST).exists() {
13751438
bail!("nvattest is not available in this image");
13761439
}
@@ -1380,22 +1443,15 @@ mod gpu {
13801443
warn!("failed to step system clock: {err:?}");
13811444
}
13821445
let nonce = hex::encode(rand::thread_rng().gen::<[u8; 32]>());
1383-
let output = run_command(
1384-
NVATTEST,
1385-
&[
1386-
"attest",
1387-
"--device",
1388-
"gpu",
1389-
"--verifier",
1390-
"local",
1391-
"--nonce",
1392-
&nonce,
1393-
"--format",
1394-
"json",
1395-
],
1396-
ATTESTATION_TIMEOUT,
1397-
)
1398-
.await?;
1446+
let args = nvattest_args(&nonce, proxy_url)?;
1447+
if let Some(base) = proxy_url {
1448+
info!(
1449+
"routing GPU attestation collateral through proxy: {}",
1450+
base.trim_end_matches('/')
1451+
);
1452+
}
1453+
let arg_refs = args.iter().map(String::as_str).collect::<Vec<_>>();
1454+
let output = run_command(NVATTEST, &arg_refs, ATTESTATION_TIMEOUT).await?;
13991455
if !output.stderr.is_empty() {
14001456
info!("nvattest: {}", truncated_lossy(&output.stderr, 2048));
14011457
}
@@ -1542,6 +1598,20 @@ mod gpu {
15421598
assert_eq!(nvidia_gpu_count(nvidia).unwrap(), 2);
15431599
}
15441600

1601+
#[test]
1602+
fn nvattest_cmdline_routes_collateral_through_proxy() {
1603+
let args = nvattest_cmdline("aa", Some("http://10.0.2.1:8090/"));
1604+
let joined = args.join(" ");
1605+
assert!(joined.contains("--ocsp-url http://10.0.2.1:8090/ocsp"));
1606+
assert!(joined.contains("--rim-url http://10.0.2.1:8090"));
1607+
assert!(joined.contains(&format!("--relying-party-policy {OUTPOST_POLICY}")));
1608+
1609+
let direct = nvattest_cmdline("aa", None).join(" ");
1610+
assert!(!direct.contains("--ocsp-url"));
1611+
assert!(!direct.contains("--rim-url"));
1612+
assert!(!direct.contains("--relying-party-policy"));
1613+
}
1614+
15451615
#[test]
15461616
fn basic_policy_requires_cc_and_rejects_devtools_by_default() {
15471617
let nonce = "44".repeat(32);
@@ -1844,7 +1914,11 @@ impl Stage0<'_> {
18441914
}
18451915
self.vmm.notify_q("boot.progress", "attesting GPU").await;
18461916
info!("verifying GPU TEE attestation");
1847-
let attestation = gpu::attest_gpu(expected_devices).await?;
1917+
let attestation = gpu::attest_gpu(
1918+
expected_devices,
1919+
self.shared.sys_config.gpu_attest_proxy_url.as_deref(),
1920+
)
1921+
.await?;
18481922

18491923
let gpu_state = gpu::query_gpu_state(expected_devices)?;
18501924
attestation

0 commit comments

Comments
 (0)