mirror of
https://github.com/ultravioletrs/cocos.git
synced 2026-06-23 04:10:25 +00:00
Added GPU evidence verification
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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[:]
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user