Added GPU evidence verification

This commit is contained in:
Jovan Djukic
2026-04-22 21:49:38 +02:00
parent a1863917b9
commit 24b526af74
9 changed files with 674 additions and 31 deletions
+63 -5
View File
@@ -4,24 +4,36 @@
package atls
import (
"bytes"
"crypto/sha256"
"fmt"
"os"
"time"
eaattestation "github.com/ultravioletrs/cocos/pkg/atls/eaattestation"
cocosattestation "github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
"github.com/ultravioletrs/cocos/pkg/attestation/gpu"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"github.com/veraison/corim/corim"
)
type policyEvidenceVerifier struct {
policyPath string
policyPath string
loadManifest func(string) (*corim.UnsignedCorim, error)
rootVerifier func(cocosattestation.PlatformType) (cocosattestation.Verifier, error)
newGPUVerifier func() (cocosattestation.Verifier, error)
}
func NewEvidenceVerifier(policyPath string) eaattestation.EvidenceVerifier {
return &policyEvidenceVerifier{policyPath: policyPath}
return &policyEvidenceVerifier{
policyPath: policyPath,
loadManifest: loadCoRIM,
rootVerifier: platformVerifier,
newGPUVerifier: defaultGPUVerifier,
}
}
func (v *policyEvidenceVerifier) VerifyEvidence(evidence []byte) error {
@@ -32,15 +44,25 @@ func (v *policyEvidenceVerifier) VerifyEvidence(evidence []byte) error {
if err != nil {
return fmt.Errorf("atls: failed to decode EAT evidence: %w", err)
}
manifest, err := loadCoRIM(v.policyPath)
manifest, err := v.loadManifest(v.policyPath)
if err != nil {
return err
}
verifier, err := platformVerifier(platformTypeFromClaims(claims.PlatformType))
verifier, err := v.rootVerifier(platformTypeFromClaims(claims.PlatformType))
if err != nil {
return err
}
return verifier.VerifyWithCoRIM(claims.RawReport, manifest)
if err := verifier.VerifyWithCoRIM(claims.RawReport, manifest); err != nil {
return err
}
if claims.GPUExtensions != nil {
if err := v.verifyGPUEvidence(claims, manifest); err != nil {
return err
}
}
return nil
}
func loadCoRIM(path string) (*corim.UnsignedCorim, error) {
@@ -90,3 +112,39 @@ func platformVerifier(platformType cocosattestation.PlatformType) (cocosattestat
return nil, fmt.Errorf("atls: unsupported platform type: %d", platformType)
}
}
func defaultGPUVerifier() (cocosattestation.Verifier, error) {
timeout := 30 * time.Second
if rawTimeout := os.Getenv("ATLS_GPU_VERIFIER_TIMEOUT"); rawTimeout != "" {
parsed, err := time.ParseDuration(rawTimeout)
if err != nil {
return nil, fmt.Errorf("atls: invalid ATLS_GPU_VERIFIER_TIMEOUT: %w", err)
}
timeout = parsed
}
binaryPath := os.Getenv("ATLS_GPU_VERIFIER_PATH")
if binaryPath == "" {
binaryPath = os.Getenv("ATTESTATION_GPU_HELPER_PATH")
}
return gpu.NewVerifier(binaryPath, timeout)
}
func (v *policyEvidenceVerifier) verifyGPUEvidence(claims *eat.EATClaims, manifest *corim.UnsignedCorim) error {
if len(claims.GPUExtensions.EvidenceJSON) == 0 {
return fmt.Errorf("atls: gpu evidence is empty")
}
expectedNonce := sha256.Sum256(append(append([]byte(nil), claims.Nonce...), []byte(":gpu")...))
if !bytes.Equal(claims.GPUExtensions.Nonce, expectedNonce[:]) {
return fmt.Errorf("atls: gpu nonce binding mismatch")
}
verifier, err := v.newGPUVerifier()
if err != nil {
return err
}
return verifier.VerifyWithCoRIM(claims.GPUExtensions.EvidenceJSON, manifest)
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package atls
import (
"crypto/sha256"
"errors"
"testing"
"github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
cocosattestation "github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
"github.com/veraison/corim/corim"
)
type stubVerifier struct {
reports [][]byte
err error
}
func (s *stubVerifier) VerifyWithCoRIM(report []byte, _ *corim.UnsignedCorim) error {
s.reports = append(s.reports, append([]byte(nil), report...))
return s.err
}
func TestPolicyEvidenceVerifierVerifyEvidence_RootOnly(t *testing.T) {
root := &stubVerifier{}
gpu := &stubVerifier{}
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: []byte("session-nonce"),
}))
require.NoError(t, err)
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
assert.Empty(t, gpu.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_RootAndGPU(t *testing.T) {
root := &stubVerifier{}
gpu := &stubVerifier{}
sessionNonce := []byte("session-nonce")
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: sessionNonce,
GPUExtensions: &eat.GPUExtensions{
Nonce: deriveExpectedGPUNonce(sessionNonce),
EvidenceJSON: []byte(`[{"nonce":"aabbcc","evidence":"abc","certificate":"def"}]`),
},
}))
require.NoError(t, err)
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
assert.Equal(t, [][]byte{[]byte(`[{"nonce":"aabbcc","evidence":"abc","certificate":"def"}]`)}, gpu.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_GPUNonceMismatch(t *testing.T) {
root := &stubVerifier{}
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: []byte("session-nonce"),
GPUExtensions: &eat.GPUExtensions{
Nonce: []byte("wrong"),
EvidenceJSON: []byte(`[{"nonce":"aabbcc"}]`),
},
}))
require.Error(t, err)
assert.ErrorContains(t, err, "gpu nonce binding mismatch")
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_GPUVerifierError(t *testing.T) {
expectedErr := errors.New("gpu verify failed")
root := &stubVerifier{}
gpu := &stubVerifier{err: expectedErr}
sessionNonce := []byte("session-nonce")
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: sessionNonce,
GPUExtensions: &eat.GPUExtensions{
Nonce: deriveExpectedGPUNonce(sessionNonce),
EvidenceJSON: []byte(`[{"nonce":"aabbcc"}]`),
},
}))
require.Error(t, err)
assert.ErrorIs(t, err, expectedErr)
}
func encodeClaims(t *testing.T, claims *eat.EATClaims) []byte {
t.Helper()
b, err := cbor.Marshal(claims)
require.NoError(t, err)
return b
}
func deriveExpectedGPUNonce(sessionNonce []byte) []byte {
sum := sha256.Sum256(append(append([]byte(nil), sessionNonce...), []byte(":gpu")...))
return sum[:]
}
+9 -4
View File
@@ -39,13 +39,17 @@ type commandCollector struct {
}
type helperRequest struct {
NonceHex string `json:"nonce_hex"`
Mode string `json:"mode,omitempty"`
NonceHex string `json:"nonce_hex"`
EvidenceJSON json.RawMessage `json:"evidence_json,omitempty"`
}
type helperResponse struct {
Vendor string `json:"vendor,omitempty"`
EvidenceFormat string `json:"evidence_format,omitempty"`
EvidenceJSON json.RawMessage `json:"evidence_json"`
Vendor string `json:"vendor,omitempty"`
EvidenceFormat string `json:"evidence_format,omitempty"`
EvidenceJSON json.RawMessage `json:"evidence_json,omitempty"`
ClaimsJSON json.RawMessage `json:"claims_json,omitempty"`
DetachedEATJSON json.RawMessage `json:"detached_eat_json,omitempty"`
}
// NewCommandCollector creates a collector that shells out to a helper binary.
@@ -72,6 +76,7 @@ func (c *commandCollector) Collect(ctx context.Context, nonce []byte) (*Evidence
}
reqBody, err := json.Marshal(helperRequest{
Mode: "collect",
NonceHex: hex.EncodeToString(nonce),
})
if err != nil {
+4
View File
@@ -57,6 +57,10 @@ func TestGPUHelperProcess(t *testing.T) {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if req.Mode != "collect" {
fmt.Fprintln(os.Stderr, "unexpected helper mode")
os.Exit(1)
}
resp := helperResponse{
Vendor: "nvidia",
+127
View File
@@ -0,0 +1,127 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gpu
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/veraison/corim/corim"
)
const (
DefaultVerifierBinary = "nvidia-attestation-helper"
defaultVerifierTimeout = 30 * time.Second
)
var _ attestation.Verifier = (*verifier)(nil)
type verifier struct {
binaryPath string
timeout time.Duration
execCommandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
}
type evidenceEnvelope struct {
Nonce string `json:"nonce"`
}
func NewVerifier(binaryPath string, timeout time.Duration) (attestation.Verifier, error) {
if strings.TrimSpace(binaryPath) == "" {
binaryPath = DefaultVerifierBinary
}
if timeout <= 0 {
timeout = defaultVerifierTimeout
}
return &verifier{
binaryPath: binaryPath,
timeout: timeout,
execCommandContext: exec.CommandContext,
}, nil
}
func (v *verifier) VerifyWithCoRIM(report []byte, manifest *corim.UnsignedCorim) error {
if len(report) == 0 {
return fmt.Errorf("gpu evidence is empty")
}
nonceHex, err := evidenceNonce(report)
if err != nil {
return err
}
reqBody, err := json.Marshal(helperRequest{
Mode: "verify",
NonceHex: nonceHex,
EvidenceJSON: append(json.RawMessage(nil), report...),
})
if err != nil {
return fmt.Errorf("failed to marshal GPU verifier request: %w", err)
}
runCtx := context.Background()
cancel := func() {}
if v.timeout > 0 {
runCtx, cancel = context.WithTimeout(runCtx, v.timeout)
}
defer cancel()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd := v.execCommandContext(runCtx, v.binaryPath)
cmd.Stdin = bytes.NewReader(reqBody)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg == "" {
errMsg = err.Error()
}
return fmt.Errorf("gpu verifier helper failed: %s", errMsg)
}
var resp helperResponse
if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
return fmt.Errorf("failed to decode GPU verifier response: %w", err)
}
if len(resp.ClaimsJSON) == 0 {
return fmt.Errorf("gpu verifier response did not contain claims_json")
}
// NVIDIA attestation currently performs its own evidence-policy appraisal
// and returns claims/detached EAT. We keep the attestation.Verifier
// interface by treating manifest integration as a follow-up layer.
_ = manifest
return nil
}
func evidenceNonce(report []byte) (string, error) {
var envelopes []evidenceEnvelope
if err := json.Unmarshal(report, &envelopes); err != nil {
return "", fmt.Errorf("failed to parse GPU evidence JSON: %w", err)
}
if len(envelopes) == 0 {
return "", fmt.Errorf("gpu evidence did not contain any devices")
}
if strings.TrimSpace(envelopes[0].Nonce) == "" {
return "", fmt.Errorf("gpu evidence nonce is missing")
}
return envelopes[0].Nonce, nil
}
// SetExecCommandContext allows tests to inject a mock exec.CommandContext.
func (v *verifier) SetExecCommandContext(cmdFunc func(ctx context.Context, name string, arg ...string) *exec.Cmd) {
v.execCommandContext = cmdFunc
}
+184
View File
@@ -0,0 +1,184 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gpu
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/veraison/corim/corim"
)
func fakeVerifierExecCommandContext(_ context.Context, name string, arg ...string) *exec.Cmd {
args := append([]string{"-test.run=TestGPUVerifierHelperProcess", "--", name}, arg...)
cmd := exec.Command(os.Args[0], args...)
cmd.Env = append(os.Environ(), "GO_WANT_GPU_VERIFIER_PROCESS=1")
return cmd
}
func TestGPUVerifierHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_GPU_VERIFIER_PROCESS") != "1" {
return
}
args := os.Args
for i := range args {
if args[i] == "--" {
args = args[i+1:]
break
}
}
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "missing verifier binary name")
os.Exit(2)
}
switch args[0] {
case "verifier-error":
fmt.Fprintln(os.Stderr, "simulated verifier failure")
os.Exit(1)
case "verifier-invalid-json":
fmt.Fprintln(os.Stdout, "{not-json")
os.Exit(0)
case "verifier-empty-claims":
fmt.Fprintln(os.Stdout, `{"detached_eat_json":{"overall_result":true}}`)
os.Exit(0)
default:
var req helperRequest
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if req.Mode != "verify" {
fmt.Fprintln(os.Stderr, "unexpected verifier mode")
os.Exit(1)
}
if req.NonceHex == "" {
fmt.Fprintln(os.Stderr, "nonce not propagated to verifier helper")
os.Exit(1)
}
if !json.Valid(req.EvidenceJSON) {
fmt.Fprintln(os.Stderr, "invalid evidence_json payload")
os.Exit(1)
}
if !containsNonce(req.EvidenceJSON, req.NonceHex) {
fmt.Fprintln(os.Stderr, "nonce not propagated to verifier")
os.Exit(1)
}
resp := helperResponse{
ClaimsJSON: json.RawMessage(`[{"x-nvidia-device-type":"gpu"}]`),
DetachedEATJSON: json.RawMessage(`{"overall_result":true}`),
}
if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}
}
func TestEvidenceNonce(t *testing.T) {
nonce, err := evidenceNonce([]byte(`[{"nonce":"aabbcc"}]`))
assert.NoError(t, err)
assert.Equal(t, "aabbcc", nonce)
_, err = evidenceNonce([]byte(`[]`))
assert.ErrorContains(t, err, "did not contain any devices")
_, err = evidenceNonce([]byte(`[{}]`))
assert.ErrorContains(t, err, "nonce is missing")
}
func containsNonce(report json.RawMessage, nonce string) bool {
var envelopes []evidenceEnvelope
if err := json.Unmarshal(report, &envelopes); err != nil {
return false
}
if len(envelopes) == 0 {
return false
}
return envelopes[0].Nonce == nonce
}
func TestVerifierVerifyWithCoRIM(t *testing.T) {
v, err := NewVerifier("verifier-success", 0)
require.NoError(t, err)
cmdVerifier, ok := v.(*verifier)
require.True(t, ok)
cmdVerifier.SetExecCommandContext(fakeVerifierExecCommandContext)
report := []byte(`[{"nonce":"aabbcc","evidence":"abc","certificate":"def"}]`)
err = v.VerifyWithCoRIM(report, &corim.UnsignedCorim{})
assert.NoError(t, err)
}
func TestVerifierVerifyWithCoRIMErrors(t *testing.T) {
tests := []struct {
name string
binary string
report []byte
wantError string
}{
{
name: "empty report",
report: nil,
wantError: "gpu evidence is empty",
},
{
name: "invalid json",
report: []byte(`{`),
wantError: "failed to parse GPU evidence JSON",
},
{
name: "helper failure",
binary: "verifier-error",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "gpu verifier helper failed",
},
{
name: "invalid verifier response",
binary: "verifier-invalid-json",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "failed to decode GPU verifier response",
},
{
name: "missing claims",
binary: "verifier-empty-claims",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "gpu verifier response did not contain claims_json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.name == "empty report" || tt.name == "invalid json" {
v, err := NewVerifier("verifier-success", 0)
require.NoError(t, err)
err = v.VerifyWithCoRIM(tt.report, nil)
assert.ErrorContains(t, err, tt.wantError)
return
}
v, err := NewVerifier(tt.binary, 0)
require.NoError(t, err)
cmdVerifier, ok := v.(*verifier)
require.True(t, ok)
cmdVerifier.SetExecCommandContext(fakeVerifierExecCommandContext)
err = v.VerifyWithCoRIM(tt.report, nil)
assert.ErrorContains(t, err, tt.wantError)
})
}
}
+1 -1
View File
@@ -6,6 +6,6 @@ license = "Apache-2.0"
[dependencies]
anyhow = "1"
nv-attestation-sdk = { git = "https://github.com/NVIDIA/attestation-sdk", branch = "main", subdirectory = "nv-attestation-sdk-rust/nv-attestation-sdk" }
nv-attestation-sdk = { git = "https://github.com/NVIDIA/attestation-sdk", branch = "main" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+32 -2
View File
@@ -1,8 +1,8 @@
# NVIDIA Attestation Helper
This helper wraps NVIDIA's Rust attestation SDK low-level GPU evidence
collection flow and exposes a tiny JSON stdin/stdout protocol that the Go
attestation service can call.
collection and verification flows and exposes a tiny JSON stdin/stdout
protocol that the Go attestation service and ATLS verifier can call.
## Request
@@ -10,10 +10,21 @@ The helper reads a single JSON object from stdin:
```json
{
"mode": "collect",
"nonce_hex": "aabbccdd"
}
```
For verification, send:
```json
{
"mode": "verify",
"nonce_hex": "aabbccdd",
"evidence_json": [{ "...": "..." }]
}
```
## Response
On success it writes:
@@ -28,6 +39,15 @@ On success it writes:
`evidence_json` is the JSON emitted by `GpuEvidence::to_json()`.
Verification responses contain the NVIDIA appraisal outputs:
```json
{
"claims_json": [{ "...": "..." }],
"detached_eat_json": { "...": "..." }
}
```
## Build
Prerequisites:
@@ -60,3 +80,13 @@ When a helper path is configured, COCOS will attempt to collect GPU evidence
opportunistically. If the host does not expose a supported CC-capable NVIDIA
GPU, the attestation service skips GPU evidence and still returns the root
CPU/TEE attestation.
ATLS can use the same helper during TLS-handshake verification:
```bash
export ATLS_GPU_VERIFIER_PATH=/path/to/nvidia-attestation-helper
export ATLS_GPU_VERIFIER_TIMEOUT=30s
```
If `ATLS_GPU_VERIFIER_PATH` is unset, the verifier also falls back to
`ATTESTATION_GPU_HELPER_PATH`.
+96 -19
View File
@@ -1,19 +1,32 @@
use anyhow::Context;
use nv_attestation_sdk::{GpuEvidence, GpuEvidenceSource, Nonce, NvatSdk};
use anyhow::{bail, Context};
use nv_attestation_sdk::{
EvidencePolicy, GpuEvidenceSource, GpuLocalVerifier, HttpOptions, Nonce, NvatSdk, OcspClient,
RimStore,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::{stdin, stdout, Write};
#[derive(Debug, Deserialize)]
struct Request {
mode: Option<String>,
nonce_hex: String,
#[serde(default)]
evidence_json: Option<Value>,
}
#[derive(Debug, Serialize)]
struct Response {
vendor: &'static str,
evidence_format: &'static str,
evidence_json: Value,
#[serde(skip_serializing_if = "Option::is_none")]
vendor: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
evidence_format: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
evidence_json: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
claims_json: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
detached_eat_json: Option<Value>,
}
fn main() -> anyhow::Result<()> {
@@ -23,20 +36,10 @@ fn main() -> anyhow::Result<()> {
serde_json::from_reader(stdin().lock()).context("failed to decode helper request")?;
let nonce = Nonce::from_hex(&req.nonce_hex).context("failed to parse nonce")?;
let evidence_source =
GpuEvidenceSource::create_nvml().context("failed to create NVML evidence source")?;
let evidence = GpuEvidence::collect(&evidence_source, Some(&nonce))
.context("failed to collect evidence")?;
let evidence_json = evidence
.to_json()
.context("failed to serialize GPU evidence to JSON")?;
let evidence_json: Value =
serde_json::from_str(&evidence_json).context("failed to parse serialized evidence JSON")?;
let resp = Response {
vendor: "nvidia",
evidence_format: "nvat-json",
evidence_json,
let resp = match req.mode.as_deref().unwrap_or("collect") {
"collect" => collect_evidence(&nonce)?,
"verify" => verify_evidence(&nonce, req.evidence_json)?,
other => bail!("unsupported helper mode: {other}"),
};
let mut out = stdout().lock();
@@ -45,3 +48,77 @@ fn main() -> anyhow::Result<()> {
Ok(())
}
fn collect_evidence(nonce: &Nonce) -> anyhow::Result<Response> {
let evidence_source =
GpuEvidenceSource::from_nvml().context("failed to create NVML evidence source")?;
let evidence = evidence_source
.collect(nonce)
.context("failed to collect evidence")?;
let evidence_json = evidence
.to_json()
.context("failed to serialize GPU evidence to JSON")?;
let evidence_json: Value =
serde_json::from_str(&evidence_json).context("failed to parse serialized evidence JSON")?;
Ok(Response {
vendor: Some("nvidia"),
evidence_format: Some("nvat-json"),
evidence_json: Some(evidence_json),
claims_json: None,
detached_eat_json: None,
})
}
fn verify_evidence(nonce: &Nonce, evidence_json: Option<Value>) -> anyhow::Result<Response> {
let evidence_json = evidence_json.context("verify mode requires evidence_json")?;
let evidence_json = serde_json::to_string(&evidence_json)
.context("failed to serialize evidence_json request field")?;
let evidence_source = GpuEvidenceSource::from_json_string(&evidence_json)
.context("failed to create JSON evidence source")?;
let evidence = evidence_source
.collect(nonce)
.context("failed to load GPU evidence from JSON")?;
if evidence.is_empty() {
bail!("GPU evidence did not contain any devices");
}
let http_opts = HttpOptions::builder()
.max_retry_count(5)
.connection_timeout_ms(10000)
.request_timeout_ms(30000)
.build()
.context("failed to create HTTP options")?;
let rim_store = RimStore::create_remote(None, None, Some(&http_opts))
.context("failed to create RIM store")?;
let ocsp_client = OcspClient::create_default(None, None, Some(&http_opts))
.context("failed to create OCSP client")?;
let verifier = GpuLocalVerifier::new(&rim_store, &ocsp_client)
.context("failed to create GPU local verifier")?;
let policy = EvidencePolicy::builder()
.verify_rim_signature(true)
.verify_rim_cert_chain(true)
.build()
.context("failed to create evidence policy")?;
let result = verifier
.verify(&evidence, &policy)
.context("failed to verify GPU evidence")?;
let claims_json =
serde_json::from_str(&result.claims_json()?).context("failed to parse claims JSON")?;
let detached_eat_json = result
.eat_json()
.ok()
.map(|raw| serde_json::from_str(&raw).context("failed to parse detached EAT JSON"))
.transpose()?;
Ok(Response {
vendor: None,
evidence_format: None,
evidence_json: None,
claims_json: Some(claims_json),
detached_eat_json,
})
}