mirror of
https://github.com/ultravioletrs/cocos.git
synced 2026-06-23 04:10:25 +00:00
Added GPU evidence collection
This commit is contained in:
+2
-1
@@ -28,4 +28,5 @@ Cargo.lock
|
||||
|
||||
*.enc
|
||||
*.key
|
||||
*.pub
|
||||
*.pub
|
||||
.codex
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
attestationpb "github.com/ultravioletrs/cocos/internal/proto/attestation/v1"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
|
||||
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
|
||||
)
|
||||
|
||||
func newGPUCollector(cfg config) (attestationgpu.Collector, error) {
|
||||
if strings.TrimSpace(cfg.GPUHelperPath) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return attestationgpu.NewCommandCollector(cfg.GPUHelperPath, cfg.GPUHelperTimeout)
|
||||
}
|
||||
|
||||
func (s *service) claimOptions(ctx context.Context, req *attestationpb.AttestationRequest, platformType attestation.PlatformType) ([]eat.ClaimsOption, error) {
|
||||
var opts []eat.ClaimsOption
|
||||
|
||||
if s.gpuCollector != nil && shouldCollectGPU(platformType) {
|
||||
sessionNonce := requestNonce(req)
|
||||
gpuNonce := deriveComponentNonce(sessionNonce, "gpu")
|
||||
|
||||
evidence, err := s.gpuCollector.Collect(ctx, gpuNonce)
|
||||
if err != nil {
|
||||
// GPU evidence is opportunistic: if no supported CC-capable GPU is
|
||||
// attached, or the helper cannot collect evidence, we continue with
|
||||
// the root CPU/TEE attestation instead of failing the whole request.
|
||||
s.logger.Warn(fmt.Sprintf("[ATTESTATION-SERVICE] Skipping optional GPU evidence collection: %s", err))
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
s.logger.Info(fmt.Sprintf("[ATTESTATION-SERVICE] Collected GPU evidence: format=%s bytes=%d",
|
||||
evidence.EvidenceFormat, len(evidence.RawEvidence)))
|
||||
|
||||
opts = append(opts, eat.WithGPU(&eat.GPUExtensions{
|
||||
Vendor: evidence.Vendor,
|
||||
EvidenceFormat: evidence.EvidenceFormat,
|
||||
Nonce: evidence.Nonce,
|
||||
EvidenceJSON: evidence.RawEvidence,
|
||||
}))
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func shouldCollectGPU(platformType attestation.PlatformType) bool {
|
||||
switch platformType {
|
||||
case attestation.SNP, attestation.SNPvTPM, attestation.TDX, attestation.Azure:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func requestNonce(req *attestationpb.AttestationRequest) []byte {
|
||||
if len(req.Nonce) > 0 {
|
||||
return append([]byte(nil), req.Nonce...)
|
||||
}
|
||||
|
||||
return append([]byte(nil), req.ReportData...)
|
||||
}
|
||||
|
||||
func deriveComponentNonce(sessionNonce []byte, component string) []byte {
|
||||
digest := sha256.Sum256(append(append([]byte(nil), sessionNonce...), []byte(":"+component)...))
|
||||
return digest[:]
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
attestationpb "github.com/ultravioletrs/cocos/internal/proto/attestation/v1"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation"
|
||||
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
|
||||
)
|
||||
|
||||
func TestRequestNonce(t *testing.T) {
|
||||
req := &attestationpb.AttestationRequest{
|
||||
ReportData: []byte("report"),
|
||||
Nonce: []byte("nonce"),
|
||||
}
|
||||
|
||||
assert.Equal(t, []byte("nonce"), requestNonce(req))
|
||||
|
||||
req.Nonce = nil
|
||||
assert.Equal(t, []byte("report"), requestNonce(req))
|
||||
}
|
||||
|
||||
func TestDeriveComponentNonce(t *testing.T) {
|
||||
sessionNonce := []byte("session-nonce")
|
||||
|
||||
gpuNonce := deriveComponentNonce(sessionNonce, "gpu")
|
||||
gpuNonceAgain := deriveComponentNonce(sessionNonce, "gpu")
|
||||
teeNonce := deriveComponentNonce(sessionNonce, "tee")
|
||||
|
||||
assert.Len(t, gpuNonce, 32)
|
||||
assert.Equal(t, gpuNonce, gpuNonceAgain)
|
||||
assert.NotEqual(t, gpuNonce, teeNonce)
|
||||
}
|
||||
|
||||
func TestShouldCollectGPU(t *testing.T) {
|
||||
assert.True(t, shouldCollectGPU(attestation.SNP))
|
||||
assert.True(t, shouldCollectGPU(attestation.SNPvTPM))
|
||||
assert.True(t, shouldCollectGPU(attestation.TDX))
|
||||
assert.False(t, shouldCollectGPU(attestation.VTPM))
|
||||
assert.False(t, shouldCollectGPU(attestation.NoCC))
|
||||
}
|
||||
|
||||
func TestNewGPUCollector(t *testing.T) {
|
||||
collector, err := newGPUCollector(config{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, collector)
|
||||
|
||||
collector, err = newGPUCollector(config{
|
||||
GPUHelperPath: "/tmp/helper",
|
||||
GPUHelperTimeout: 0,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, collector)
|
||||
}
|
||||
|
||||
func TestClaimOptions_SkipsOptionalGPUFailure(t *testing.T) {
|
||||
svc := &service{
|
||||
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
gpuCollector: failingCollector{},
|
||||
}
|
||||
|
||||
req := &attestationpb.AttestationRequest{
|
||||
ReportData: []byte("report-data"),
|
||||
Nonce: []byte("nonce-data"),
|
||||
}
|
||||
|
||||
opts, err := svc.claimOptions(context.Background(), req, attestation.TDX)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, opts)
|
||||
}
|
||||
|
||||
type failingCollector struct{}
|
||||
|
||||
func (failingCollector) Collect(context.Context, []byte) (*attestationgpu.Evidence, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
mglog "github.com/absmach/supermq/logger"
|
||||
"github.com/caarlos0/env/v11"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/ccaa"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
|
||||
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
|
||||
logclient "github.com/ultravioletrs/cocos/pkg/clients/grpc/log"
|
||||
@@ -35,16 +37,18 @@ const (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
LogLevel string `env:"ATTESTATION_LOG_LEVEL" envDefault:"debug"`
|
||||
Vmpl int `env:"ATTESTATION_VMPL" envDefault:"2"`
|
||||
AgentMaaURL string `env:"AGENT_MAA_URL" envDefault:"https://sharedeus2.eus2.attest.azure.net"`
|
||||
AgentOSBuild string `env:"AGENT_OS_BUILD" envDefault:"UVC"`
|
||||
AgentOSDistro string `env:"AGENT_OS_DISTRO" envDefault:"UVC"`
|
||||
AgentOSType string `env:"AGENT_OS_TYPE" envDefault:"UVC"`
|
||||
EATFormat string `env:"ATTESTATION_EAT_FORMAT" envDefault:"CBOR"` // JWT or CBOR
|
||||
EATIssuer string `env:"ATTESTATION_EAT_ISSUER" envDefault:"cocos-attestation-service"`
|
||||
UseCCAttestationAgent bool `env:"USE_CC_ATTESTATION_AGENT" envDefault:"false"`
|
||||
CCAgentAddress string `env:"CC_AGENT_ADDRESS" envDefault:"127.0.0.1:50002"`
|
||||
LogLevel string `env:"ATTESTATION_LOG_LEVEL" envDefault:"debug"`
|
||||
Vmpl int `env:"ATTESTATION_VMPL" envDefault:"2"`
|
||||
AgentMaaURL string `env:"AGENT_MAA_URL" envDefault:"https://sharedeus2.eus2.attest.azure.net"`
|
||||
AgentOSBuild string `env:"AGENT_OS_BUILD" envDefault:"UVC"`
|
||||
AgentOSDistro string `env:"AGENT_OS_DISTRO" envDefault:"UVC"`
|
||||
AgentOSType string `env:"AGENT_OS_TYPE" envDefault:"UVC"`
|
||||
EATFormat string `env:"ATTESTATION_EAT_FORMAT" envDefault:"CBOR"` // JWT or CBOR
|
||||
EATIssuer string `env:"ATTESTATION_EAT_ISSUER" envDefault:"cocos-attestation-service"`
|
||||
UseCCAttestationAgent bool `env:"USE_CC_ATTESTATION_AGENT" envDefault:"false"`
|
||||
CCAgentAddress string `env:"CC_AGENT_ADDRESS" envDefault:"127.0.0.1:50002"`
|
||||
GPUHelperPath string `env:"ATTESTATION_GPU_HELPER_PATH" envDefault:""`
|
||||
GPUHelperTimeout time.Duration `env:"ATTESTATION_GPU_HELPER_TIMEOUT" envDefault:"30s"`
|
||||
|
||||
// Future KBS Integration Configuration
|
||||
// When KBS support is added, these fields will enable:
|
||||
@@ -229,13 +233,24 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
gpuCollector, err := newGPUCollector(cfg)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to configure GPU attestation collector: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
if gpuCollector != nil {
|
||||
logger.Info(fmt.Sprintf("[ATTESTATION-SERVICE] GPU evidence collection enabled via helper %s", cfg.GPUHelperPath))
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer()
|
||||
svc := &service{
|
||||
provider: provider,
|
||||
logger: logger,
|
||||
signingKey: signingKey,
|
||||
eatFormat: cfg.EATFormat,
|
||||
eatIssuer: cfg.EATIssuer,
|
||||
provider: provider,
|
||||
logger: logger,
|
||||
signingKey: signingKey,
|
||||
eatFormat: cfg.EATFormat,
|
||||
eatIssuer: cfg.EATIssuer,
|
||||
gpuCollector: gpuCollector,
|
||||
}
|
||||
attestationpb.RegisterAttestationServiceServer(grpcServer, svc)
|
||||
|
||||
@@ -267,11 +282,12 @@ func main() {
|
||||
|
||||
type service struct {
|
||||
attestationpb.UnimplementedAttestationServiceServer
|
||||
provider attestation.Provider
|
||||
logger *slog.Logger
|
||||
signingKey *ecdsa.PrivateKey
|
||||
eatFormat string
|
||||
eatIssuer string
|
||||
provider attestation.Provider
|
||||
logger *slog.Logger
|
||||
signingKey *ecdsa.PrivateKey
|
||||
eatFormat string
|
||||
eatIssuer string
|
||||
gpuCollector attestationgpu.Collector
|
||||
}
|
||||
|
||||
func (s *service) FetchAttestation(ctx context.Context, req *attestationpb.AttestationRequest) (*attestationpb.AttestationResponse, error) {
|
||||
@@ -330,12 +346,14 @@ func (s *service) FetchAttestation(ctx context.Context, req *attestationpb.Attes
|
||||
}
|
||||
|
||||
// Create EAT claims from binary report
|
||||
nonce := req.ReportData
|
||||
if len(req.Nonce) > 0 {
|
||||
nonce = req.Nonce
|
||||
nonce := requestNonce(req)
|
||||
|
||||
claimOpts, err := s.claimOptions(ctx, req, platformType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, err := eat.NewEATClaims(binaryReport, nonce, platformType)
|
||||
claims, err := eat.NewEATClaims(binaryReport, nonce, platformType, claimOpts...)
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("failed to create EAT claims: %s", err))
|
||||
return nil, fmt.Errorf("failed to create EAT claims: %w", err)
|
||||
|
||||
@@ -39,6 +39,7 @@ type EATClaims struct {
|
||||
SNPExtensions *SNPExtensions `json:"x-cocos-sevsnp,omitempty"`
|
||||
TDXExtensions *TDXExtensions `json:"x-cocos-tdx,omitempty"`
|
||||
VTPMExtensions *VTPMExtensions `json:"x-cocos-vtpm,omitempty"`
|
||||
GPUExtensions *GPUExtensions `json:"x-cocos-gpu,omitempty"`
|
||||
|
||||
// Original binary report (for verification)
|
||||
RawReport []byte `json:"raw_report,omitempty"`
|
||||
@@ -94,6 +95,36 @@ type VTPMExtensions struct {
|
||||
Quote []byte `json:"quote,omitempty"` // TPM quote
|
||||
}
|
||||
|
||||
// GPUExtensions contains optional GPU attestation evidence that is bound to the
|
||||
// same attestation session as the root TEE evidence.
|
||||
type GPUExtensions struct {
|
||||
Vendor string `json:"vendor,omitempty" cbor:"vendor,omitempty"`
|
||||
EvidenceFormat string `json:"evidence_format,omitempty" cbor:"evidence_format,omitempty"`
|
||||
Nonce []byte `json:"nonce,omitempty" cbor:"nonce,omitempty"`
|
||||
EvidenceJSON []byte `json:"evidence_json,omitempty" cbor:"evidence_json,omitempty"`
|
||||
}
|
||||
|
||||
// ClaimsOption customizes EAT claims after the root platform claims are
|
||||
// extracted.
|
||||
type ClaimsOption func(*EATClaims) error
|
||||
|
||||
// WithGPU attaches GPU evidence both as a typed extension and as an EAT submod.
|
||||
func WithGPU(gpu *GPUExtensions) ClaimsOption {
|
||||
return func(claims *EATClaims) error {
|
||||
if gpu == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
claims.GPUExtensions = gpu
|
||||
if claims.Submods == nil {
|
||||
claims.Submods = map[string]interface{}{}
|
||||
}
|
||||
claims.Submods["gpu"] = gpu
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DebugStatus constants (RFC 9711 Section 4.2.6).
|
||||
const (
|
||||
DebugEnabled = 0 // Debug is enabled
|
||||
@@ -112,7 +143,7 @@ const (
|
||||
const MinNonceLength = 8
|
||||
|
||||
// NewEATClaims creates EAT claims from binary attestation report.
|
||||
func NewEATClaims(report []byte, nonce []byte, platformType attestation.PlatformType) (*EATClaims, error) {
|
||||
func NewEATClaims(report []byte, nonce []byte, platformType attestation.PlatformType, opts ...ClaimsOption) (*EATClaims, error) {
|
||||
if len(nonce) < MinNonceLength {
|
||||
return nil, errors.New("eat_nonce must be at least 8 bytes long")
|
||||
}
|
||||
@@ -129,6 +160,12 @@ func NewEATClaims(report []byte, nonce []byte, platformType attestation.Platform
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -202,3 +202,24 @@ func TestNewEATClaims_Platforms(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEATClaims_WithGPU(t *testing.T) {
|
||||
gpuEvidence := &GPUExtensions{
|
||||
Vendor: "nvidia",
|
||||
EvidenceFormat: "nvat-json",
|
||||
Nonce: []byte("gpu-nonce"),
|
||||
EvidenceJSON: []byte(`{"evidence":"gpu"}`),
|
||||
}
|
||||
|
||||
claims, err := NewEATClaims(
|
||||
[]byte("dummy report"),
|
||||
[]byte("12345678"),
|
||||
attestation.NoCC,
|
||||
WithGPU(gpuEvidence),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, claims.GPUExtensions)
|
||||
assert.Equal(t, gpuEvidence, claims.GPUExtensions)
|
||||
assert.Contains(t, claims.Submods, "gpu")
|
||||
assert.Equal(t, gpuEvidence, claims.Submods["gpu"])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultVendor = "nvidia"
|
||||
DefaultEvidenceFormat = "nvat-json"
|
||||
)
|
||||
|
||||
// Collector retrieves GPU evidence for the current attestation session.
|
||||
type Collector interface {
|
||||
Collect(ctx context.Context, nonce []byte) (*Evidence, error)
|
||||
}
|
||||
|
||||
// Evidence contains low-level GPU evidence collected out-of-process.
|
||||
type Evidence struct {
|
||||
Vendor string
|
||||
EvidenceFormat string
|
||||
Nonce []byte
|
||||
RawEvidence []byte
|
||||
}
|
||||
|
||||
type commandCollector struct {
|
||||
binaryPath string
|
||||
timeout time.Duration
|
||||
execCommandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
|
||||
}
|
||||
|
||||
type helperRequest struct {
|
||||
NonceHex string `json:"nonce_hex"`
|
||||
}
|
||||
|
||||
type helperResponse struct {
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
EvidenceFormat string `json:"evidence_format,omitempty"`
|
||||
EvidenceJSON json.RawMessage `json:"evidence_json"`
|
||||
}
|
||||
|
||||
// NewCommandCollector creates a collector that shells out to a helper binary.
|
||||
// The helper is expected to read a JSON request on stdin and emit a JSON
|
||||
// response on stdout. See tools/nvidia-attestation-helper for the contract.
|
||||
func NewCommandCollector(binaryPath string, timeout time.Duration) (Collector, error) {
|
||||
if strings.TrimSpace(binaryPath) == "" {
|
||||
return nil, fmt.Errorf("gpu helper path cannot be empty")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return &commandCollector{
|
||||
binaryPath: binaryPath,
|
||||
timeout: timeout,
|
||||
execCommandContext: exec.CommandContext,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *commandCollector) Collect(ctx context.Context, nonce []byte) (*Evidence, error) {
|
||||
if len(nonce) == 0 {
|
||||
return nil, fmt.Errorf("gpu nonce cannot be empty")
|
||||
}
|
||||
|
||||
reqBody, err := json.Marshal(helperRequest{
|
||||
NonceHex: hex.EncodeToString(nonce),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal GPU helper request: %w", err)
|
||||
}
|
||||
|
||||
runCtx := ctx
|
||||
cancel := func() {}
|
||||
if c.timeout > 0 {
|
||||
runCtx, cancel = context.WithTimeout(ctx, c.timeout)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
||||
cmd := c.execCommandContext(runCtx, c.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 nil, fmt.Errorf("gpu helper failed: %s", errMsg)
|
||||
}
|
||||
|
||||
var resp helperResponse
|
||||
if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode GPU helper response: %w", err)
|
||||
}
|
||||
if len(resp.EvidenceJSON) == 0 {
|
||||
return nil, fmt.Errorf("gpu helper response did not contain evidence_json")
|
||||
}
|
||||
|
||||
vendor := resp.Vendor
|
||||
if vendor == "" {
|
||||
vendor = DefaultVendor
|
||||
}
|
||||
|
||||
evidenceFormat := resp.EvidenceFormat
|
||||
if evidenceFormat == "" {
|
||||
evidenceFormat = DefaultEvidenceFormat
|
||||
}
|
||||
|
||||
return &Evidence{
|
||||
Vendor: vendor,
|
||||
EvidenceFormat: evidenceFormat,
|
||||
Nonce: append([]byte(nil), nonce...),
|
||||
RawEvidence: append([]byte(nil), resp.EvidenceJSON...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetExecCommandContext allows tests to inject a mock exec.CommandContext.
|
||||
func (c *commandCollector) SetExecCommandContext(cmdFunc func(ctx context.Context, name string, arg ...string) *exec.Cmd) {
|
||||
c.execCommandContext = cmdFunc
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func fakeExecCommandContext(_ context.Context, name string, arg ...string) *exec.Cmd {
|
||||
args := append([]string{"-test.run=TestGPUHelperProcess", "--", name}, arg...)
|
||||
cmd := exec.Command(os.Args[0], args...)
|
||||
cmd.Env = append(os.Environ(), "GO_WANT_GPU_HELPER_PROCESS=1")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestGPUHelperProcess(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_GPU_HELPER_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 helper name")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "helper-error":
|
||||
fmt.Fprintln(os.Stderr, "simulated helper failure")
|
||||
os.Exit(1)
|
||||
case "helper-invalid-json":
|
||||
fmt.Fprintln(os.Stdout, "{not-json")
|
||||
os.Exit(0)
|
||||
case "helper-empty-evidence":
|
||||
fmt.Fprintln(os.Stdout, `{"vendor":"nvidia","evidence_format":"nvat-json"}`)
|
||||
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)
|
||||
}
|
||||
|
||||
resp := helperResponse{
|
||||
Vendor: "nvidia",
|
||||
EvidenceFormat: "nvat-json",
|
||||
EvidenceJSON: json.RawMessage(fmt.Sprintf(`{"nonce_hex":"%s","evidence":"ok"}`, req.NonceHex)),
|
||||
}
|
||||
if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCommandCollector(t *testing.T) {
|
||||
collector, err := NewCommandCollector("helper", time.Second)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, collector)
|
||||
|
||||
collector, err = NewCommandCollector("", time.Second)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, collector)
|
||||
}
|
||||
|
||||
func TestCommandCollectorCollect(t *testing.T) {
|
||||
collector, err := NewCommandCollector("helper-success", time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmdCollector, ok := collector.(*commandCollector)
|
||||
require.True(t, ok)
|
||||
cmdCollector.SetExecCommandContext(fakeExecCommandContext)
|
||||
|
||||
evidence, err := collector.Collect(context.Background(), []byte{0xaa, 0xbb, 0xcc})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, DefaultVendor, evidence.Vendor)
|
||||
assert.Equal(t, DefaultEvidenceFormat, evidence.EvidenceFormat)
|
||||
assert.Equal(t, []byte{0xaa, 0xbb, 0xcc}, evidence.Nonce)
|
||||
assert.JSONEq(t, `{"nonce_hex":"aabbcc","evidence":"ok"}`, string(evidence.RawEvidence))
|
||||
}
|
||||
|
||||
func TestCommandCollectorCollectErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
helperName string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "helper process failure",
|
||||
helperName: "helper-error",
|
||||
wantErr: "gpu helper failed: simulated helper failure",
|
||||
},
|
||||
{
|
||||
name: "invalid json response",
|
||||
helperName: "helper-invalid-json",
|
||||
wantErr: "failed to decode GPU helper response",
|
||||
},
|
||||
{
|
||||
name: "missing evidence payload",
|
||||
helperName: "helper-empty-evidence",
|
||||
wantErr: "gpu helper response did not contain evidence_json",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
collector, err := NewCommandCollector(tt.helperName, time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmdCollector, ok := collector.(*commandCollector)
|
||||
require.True(t, ok)
|
||||
cmdCollector.SetExecCommandContext(fakeExecCommandContext)
|
||||
|
||||
_, err = collector.Collect(context.Background(), []byte{0xaa})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "nvidia-attestation-helper"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
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" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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.
|
||||
|
||||
## Request
|
||||
|
||||
The helper reads a single JSON object from stdin:
|
||||
|
||||
```json
|
||||
{
|
||||
"nonce_hex": "aabbccdd"
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
On success it writes:
|
||||
|
||||
```json
|
||||
{
|
||||
"vendor": "nvidia",
|
||||
"evidence_format": "nvat-json",
|
||||
"evidence_json": { "...": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
`evidence_json` is the JSON emitted by `GpuEvidence::to_json()`.
|
||||
|
||||
## Build
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Rust 1.80+
|
||||
- `libnvat.so.1`
|
||||
- Clang/LLVM
|
||||
- NVIDIA GPU driver with NVML support
|
||||
|
||||
If you are using a system-installed NVAT library:
|
||||
|
||||
```bash
|
||||
export NVAT_USE_SYSTEM_LIB=1
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
If you built NVAT locally, make sure the C library is installed or on
|
||||
`LD_LIBRARY_PATH` before building or running the helper.
|
||||
|
||||
## Use With COCOS
|
||||
|
||||
Point the attestation service at the compiled binary:
|
||||
|
||||
```bash
|
||||
export ATTESTATION_GPU_HELPER_PATH=/path/to/nvidia-attestation-helper
|
||||
export ATTESTATION_GPU_HELPER_TIMEOUT=30s
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,47 @@
|
||||
use anyhow::Context;
|
||||
use nv_attestation_sdk::{GpuEvidence, GpuEvidenceSource, Nonce, NvatSdk};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::io::{stdin, stdout, Write};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Request {
|
||||
nonce_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Response {
|
||||
vendor: &'static str,
|
||||
evidence_format: &'static str,
|
||||
evidence_json: Value,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let _sdk = NvatSdk::init_default().context("failed to initialize NVIDIA attestation SDK")?;
|
||||
|
||||
let req: Request =
|
||||
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 mut out = stdout().lock();
|
||||
serde_json::to_writer(&mut out, &resp).context("failed to write helper response")?;
|
||||
let _ = out.write_all(b"\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user