@@ -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