From 00fd0371e1f32dc99fde6b43ec0f8c3d40809344 Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Thu, 23 Apr 2026 16:22:52 -0400 Subject: [PATCH 1/8] attesters: add NVIDIA GPU evidence plugin Signed-off-by: Ian Chin Wang --- attesters/Makefile | 1 + attesters/gpu/Makefile | 10 ++ attesters/gpu/gpu.go | 214 +++++++++++++++++++++++ attesters/gpu/gpu_test.go | 311 ++++++++++++++++++++++++++++++++++ attesters/gpu/plugin/Makefile | 8 + attesters/gpu/plugin/main.go | 13 ++ go.mod | 4 +- go.sum | 7 +- tokens/gpu-evidence.go | 90 ++++++++++ tokens/gpu-evidence_test.go | 77 +++++++++ 10 files changed, 733 insertions(+), 2 deletions(-) create mode 100644 attesters/gpu/Makefile create mode 100644 attesters/gpu/gpu.go create mode 100644 attesters/gpu/gpu_test.go create mode 100644 attesters/gpu/plugin/Makefile create mode 100644 attesters/gpu/plugin/main.go create mode 100644 tokens/gpu-evidence.go create mode 100644 tokens/gpu-evidence_test.go diff --git a/attesters/Makefile b/attesters/Makefile index b1a2c83..47e6292 100644 --- a/attesters/Makefile +++ b/attesters/Makefile @@ -3,6 +3,7 @@ SUBDIR := tsm SUBDIR += mocktsm +SUBDIR += gpu clean: ; $(RM) -rf ./bin diff --git a/attesters/gpu/Makefile b/attesters/gpu/Makefile new file mode 100644 index 0000000..d536fe6 --- /dev/null +++ b/attesters/gpu/Makefile @@ -0,0 +1,10 @@ +# Copyright 2026 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 +.DEFAULT_GOAL := test + +GOPKG := github.com/veraison/ratsd/attesters/gpu +SRCS := $(wildcard *.go) + +SUBDIR += plugin + +include ../../mk/subdir.mk diff --git a/attesters/gpu/gpu.go b/attesters/gpu/gpu.go new file mode 100644 index 0000000..458f342 --- /dev/null +++ b/attesters/gpu/gpu.go @@ -0,0 +1,214 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package gpu + +import ( + "encoding/json" + "fmt" + + "github.com/NVIDIA/go-nvml/pkg/nvml" + nvgpu "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/gpu" + "github.com/veraison/ratsd/proto/compositor" + "github.com/veraison/ratsd/tokens" +) + +const ( + ApplicationvndVeraisonNvGpuEvidenceJSON = tokens.GPUEvidenceMediaTypeJSON + ApplicationvndVeraisonNvGpuEvidenceCBOR = tokens.GPUEvidenceMediaTypeCBOR + gpuNonceSize = nvml.CC_GPU_CEC_NONCE_SIZE +) + +var ( + sid = &compositor.SubAttesterID{ + Name: "nv-gpu-evidence", + Version: "1.0.0", + } + + supportedFormats = []*compositor.Format{ + { + ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, + NonceSize: gpuNonceSize, + }, + { + ContentType: ApplicationvndVeraisonNvGpuEvidenceCBOR, + NonceSize: gpuNonceSize, + }, + } + + statusSucceeded = &compositor.Status{Result: true, Error: ""} +) + +type evidenceCollector interface { + CollectEvidence(nonce []byte) ([]nvgpu.GPUDevice, error) + Shutdown() error +} + +type collectorFactory func() (evidenceCollector, error) + +type GPUPlugin struct { + newCollector collectorFactory +} + +func NewPlugin() *GPUPlugin { + return &GPUPlugin{newCollector: defaultCollectorFactory} +} + +func defaultCollectorFactory() (evidenceCollector, error) { + return nvgpu.NewNvmlGPUAdmin(nil) +} + +func getEvidenceError(e error) *compositor.EvidenceOut { + return &compositor.EvidenceOut{ + Status: &compositor.Status{ + Result: false, + Error: e.Error(), + }, + } +} + +func (g *GPUPlugin) GetOptions() *compositor.OptionsOut { + return &compositor.OptionsOut{ + Options: []*compositor.Option{}, + Status: statusSucceeded, + } +} + +func (g *GPUPlugin) GetSubAttesterID() *compositor.SubAttesterIDOut { + return &compositor.SubAttesterIDOut{ + SubAttesterID: sid, + Status: statusSucceeded, + } +} + +func (g *GPUPlugin) GetSupportedFormats() *compositor.SupportedFormatsOut { + collector, err := g.newCollector() + if err != nil { + return &compositor.SupportedFormatsOut{ + Status: &compositor.Status{ + Result: false, + Error: fmt.Sprintf("GPU evidence collection is not available: %s", err.Error()), + }, + } + } + + if err := collector.Shutdown(); err != nil { + return &compositor.SupportedFormatsOut{ + Status: &compositor.Status{ + Result: false, + Error: fmt.Sprintf("GPU evidence collection is not available: %s", err.Error()), + }, + } + } + + return &compositor.SupportedFormatsOut{ + Status: statusSucceeded, + Formats: supportedFormats, + } +} + +func (g *GPUPlugin) GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut { + if uint32(len(in.Nonce)) != gpuNonceSize { + errMsg := fmt.Errorf( + "nonce size of the GPU attester should be %d, got %d", + gpuNonceSize, uint32(len(in.Nonce)), + ) + return getEvidenceError(errMsg) + } + + if err := validateOptions(in.Options); err != nil { + return getEvidenceError(err) + } + + if !supportsFormat(in.ContentType) { + return getEvidenceError(fmt.Errorf("no supported format in gpu plugin matches the requested format")) + } + + collector, err := g.newCollector() + if err != nil { + return getEvidenceError(fmt.Errorf("failed to initialize GPU evidence collector: %v", err)) + } + + devices, collectErr := collector.CollectEvidence(in.Nonce) + shutdownErr := collector.Shutdown() + + if collectErr != nil { + return getEvidenceError(fmt.Errorf("failed to collect GPU evidence: %v", collectErr)) + } + if shutdownErr != nil { + return getEvidenceError(fmt.Errorf("failed to shutdown GPU evidence collector: %v", shutdownErr)) + } + + encodedEvidence, err := encodeEvidence(in.ContentType, in.Nonce, devices) + if err != nil { + return getEvidenceError(err) + } + + return &compositor.EvidenceOut{ + Status: statusSucceeded, + Evidence: encodedEvidence, + } +} + +func supportsFormat(contentType string) bool { + for _, format := range supportedFormats { + if format.ContentType == contentType { + return true + } + } + + return false +} + +func validateOptions(options []byte) error { + if len(options) == 0 || string(options) == "null" { + return nil + } + + var parsed map[string]json.RawMessage + if err := json.Unmarshal(options, &parsed); err != nil { + return fmt.Errorf("failed to parse %s: %v", options, err) + } + + if len(parsed) > 0 { + return fmt.Errorf("gpu attester does not support options") + } + + return nil +} + +func encodeEvidence(contentType string, nonce []byte, devices []nvgpu.GPUDevice) ([]byte, error) { + token := &tokens.GPUEvidence{ + Devices: make([]tokens.GPUDeviceEvidence, len(devices)), + } + + for i, device := range devices { + certChain, err := device.Certificate().EncodeBase64() + if err != nil { + return nil, fmt.Errorf("failed to encode GPU certificate chain for device %d: %v", i, err) + } + + token.Devices[i] = tokens.GPUDeviceEvidence{ + Nonce: nonce, + Arch: device.Arch(), + AttestationReport: device.AttestationReport(), + CertificateChain: certChain, + } + } + + switch contentType { + case ApplicationvndVeraisonNvGpuEvidenceJSON: + encodedEvidence, err := token.ToJSON() + if err != nil { + return nil, fmt.Errorf("failed to JSON encode GPU evidence: %v", err) + } + return encodedEvidence, nil + case ApplicationvndVeraisonNvGpuEvidenceCBOR: + encodedEvidence, err := token.ToCBOR() + if err != nil { + return nil, fmt.Errorf("failed to CBOR encode GPU evidence: %v", err) + } + return encodedEvidence, nil + default: + return nil, fmt.Errorf("no supported format in gpu plugin matches the requested format") + } +} diff --git a/attesters/gpu/gpu_test.go b/attesters/gpu/gpu_test.go new file mode 100644 index 0000000..dae6d51 --- /dev/null +++ b/attesters/gpu/gpu_test.go @@ -0,0 +1,311 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package gpu + +import ( + "errors" + "fmt" + "testing" + + "github.com/NVIDIA/go-nvml/pkg/nvml" + "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/certs" + nvgpu "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/gpu" + nvmocks "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/mocks" + "github.com/stretchr/testify/assert" + "github.com/veraison/ratsd/proto/compositor" + "github.com/veraison/ratsd/tokens" +) + +type fakeCollector struct { + devices []nvgpu.GPUDevice + collectErr error + shutdownErr error + collectedNonce []byte + shutdownInvoked bool +} + +func (f *fakeCollector) CollectEvidence(nonce []byte) ([]nvgpu.GPUDevice, error) { + f.collectedNonce = append([]byte(nil), nonce...) + if f.collectErr != nil { + return nil, f.collectErr + } + + return f.devices, nil +} + +func (f *fakeCollector) Shutdown() error { + f.shutdownInvoked = true + return f.shutdownErr +} + +func makePlugin(factory collectorFactory) *GPUPlugin { + return &GPUPlugin{newCollector: factory} +} + +func validGPUDevices(t *testing.T) []nvgpu.GPUDevice { + t.Helper() + + certChain := certs.NewCertChainFromData(nvmocks.ValidCertChainData) + requireErr := certChain.Verify() + assert.NoError(t, requireErr) + + return []nvgpu.GPUDevice{ + nvgpu.NewGPUDevice( + nvml.DEVICE_ARCH_HOPPER, + []byte("attestation-report"), + certChain, + ), + } +} + +func Test_GetOptions(t *testing.T) { + expected := &compositor.OptionsOut{ + Options: []*compositor.Option{}, + Status: statusSucceeded, + } + + assert.Equal(t, expected, NewPlugin().GetOptions()) +} + +func Test_GetSubAttesterID(t *testing.T) { + expected := &compositor.SubAttesterIDOut{ + SubAttesterID: sid, + Status: statusSucceeded, + } + + assert.Equal(t, expected, NewPlugin().GetSubAttesterID()) +} + +func Test_GetSupportedFormats(t *testing.T) { + collector := &fakeCollector{} + p := makePlugin(func() (evidenceCollector, error) { + return collector, nil + }) + + expected := &compositor.SupportedFormatsOut{ + Status: statusSucceeded, + Formats: supportedFormats, + } + + assert.Equal(t, expected, p.GetSupportedFormats()) + assert.True(t, collector.shutdownInvoked) +} + +func Test_GetSupportedFormats_InitFailure(t *testing.T) { + p := makePlugin(func() (evidenceCollector, error) { + return nil, errors.New("nvml unavailable") + }) + + expected := &compositor.SupportedFormatsOut{ + Status: &compositor.Status{ + Result: false, + Error: "GPU evidence collection is not available: nvml unavailable", + }, + } + + assert.Equal(t, expected, p.GetSupportedFormats()) +} + +func Test_GetEvidence_WrongNonceSize(t *testing.T) { + in := &compositor.EvidenceIn{ + ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, + Nonce: []byte("short"), + } + + errMsg := fmt.Sprintf( + "nonce size of the GPU attester should be %d, got %d", + gpuNonceSize, len(in.Nonce), + ) + expected := &compositor.EvidenceOut{ + Status: &compositor.Status{ + Result: false, + Error: errMsg, + }, + } + + assert.Equal(t, expected, NewPlugin().GetEvidence(in)) +} + +func Test_GetEvidence_InvalidFormat(t *testing.T) { + in := &compositor.EvidenceIn{ + ContentType: "application/invalid", + Nonce: []byte("12345678901234567890123456789012"), + } + + expected := &compositor.EvidenceOut{ + Status: &compositor.Status{ + Result: false, + Error: "no supported format in gpu plugin matches the requested format", + }, + } + + assert.Equal(t, expected, NewPlugin().GetEvidence(in)) +} + +func Test_GetEvidence_InvalidOptions(t *testing.T) { + tests := []struct { + name string + opts string + msg string + }{ + { + name: "invalid json", + opts: `{"mode"}`, + msg: `failed to parse {"mode"}: invalid character '}' after object key`, + }, + { + name: "unsupported option", + opts: `{"mode":"full"}`, + msg: "gpu attester does not support options", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in := &compositor.EvidenceIn{ + ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, + Nonce: []byte("12345678901234567890123456789012"), + Options: []byte(tt.opts), + } + + expected := &compositor.EvidenceOut{ + Status: &compositor.Status{ + Result: false, + Error: tt.msg, + }, + } + + assert.Equal(t, expected, NewPlugin().GetEvidence(in)) + }) + } +} + +func Test_GetEvidence_CollectFailure(t *testing.T) { + collector := &fakeCollector{ + collectErr: errors.New("collection failed"), + } + p := makePlugin(func() (evidenceCollector, error) { + return collector, nil + }) + + in := &compositor.EvidenceIn{ + ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, + Nonce: []byte("12345678901234567890123456789012"), + } + + expected := &compositor.EvidenceOut{ + Status: &compositor.Status{ + Result: false, + Error: "failed to collect GPU evidence: collection failed", + }, + } + + assert.Equal(t, expected, p.GetEvidence(in)) + assert.True(t, collector.shutdownInvoked) +} + +func Test_GetEvidence_JSON(t *testing.T) { + collector := &fakeCollector{ + devices: validGPUDevices(t), + } + p := makePlugin(func() (evidenceCollector, error) { + return collector, nil + }) + + nonce := []byte("12345678901234567890123456789012") + in := &compositor.EvidenceIn{ + ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, + Nonce: nonce, + } + + expectedToken := &tokens.GPUEvidence{ + Devices: []tokens.GPUDeviceEvidence{ + { + Nonce: nonce, + Arch: "HOPPER", + AttestationReport: []byte("attestation-report"), + CertificateChain: mustCertChainBase64(t), + }, + }, + } + expectedEvidence, err := expectedToken.ToJSON() + assert.NoError(t, err) + + expected := &compositor.EvidenceOut{ + Status: statusSucceeded, + Evidence: expectedEvidence, + } + + assert.Equal(t, expected, p.GetEvidence(in)) + assert.Equal(t, nonce, collector.collectedNonce) + assert.True(t, collector.shutdownInvoked) +} + +func Test_GetEvidence_CBOR(t *testing.T) { + collector := &fakeCollector{ + devices: validGPUDevices(t), + } + p := makePlugin(func() (evidenceCollector, error) { + return collector, nil + }) + + nonce := []byte("12345678901234567890123456789012") + in := &compositor.EvidenceIn{ + ContentType: ApplicationvndVeraisonNvGpuEvidenceCBOR, + Nonce: nonce, + } + + expectedToken := &tokens.GPUEvidence{ + Devices: []tokens.GPUDeviceEvidence{ + { + Nonce: nonce, + Arch: "HOPPER", + AttestationReport: []byte("attestation-report"), + CertificateChain: mustCertChainBase64(t), + }, + }, + } + expectedEvidence, err := expectedToken.ToCBOR() + assert.NoError(t, err) + + expected := &compositor.EvidenceOut{ + Status: statusSucceeded, + Evidence: expectedEvidence, + } + + assert.Equal(t, expected, p.GetEvidence(in)) +} + +func Test_GetEvidence_ShutdownFailure(t *testing.T) { + collector := &fakeCollector{ + devices: validGPUDevices(t), + shutdownErr: errors.New("shutdown failed"), + } + p := makePlugin(func() (evidenceCollector, error) { + return collector, nil + }) + + in := &compositor.EvidenceIn{ + ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, + Nonce: []byte("12345678901234567890123456789012"), + } + + expected := &compositor.EvidenceOut{ + Status: &compositor.Status{ + Result: false, + Error: "failed to shutdown GPU evidence collector: shutdown failed", + }, + } + + assert.Equal(t, expected, p.GetEvidence(in)) +} + +func mustCertChainBase64(t *testing.T) string { + t.Helper() + + certChain := certs.NewCertChainFromData(nvmocks.ValidCertChainData) + encoded, err := certChain.EncodeBase64() + assert.NoError(t, err) + + return encoded +} diff --git a/attesters/gpu/plugin/Makefile b/attesters/gpu/plugin/Makefile new file mode 100644 index 0000000..c6d8aaa --- /dev/null +++ b/attesters/gpu/plugin/Makefile @@ -0,0 +1,8 @@ +# Copyright 2026 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +PLUGIN := ../../bin/gpu.plugin +GOPKG := github.com/veraison/ratsd/attesters/gpu +SRCS := main.go + +include ../../../mk/plugin.mk diff --git a/attesters/gpu/plugin/main.go b/attesters/gpu/plugin/main.go new file mode 100644 index 0000000..83f800a --- /dev/null +++ b/attesters/gpu/plugin/main.go @@ -0,0 +1,13 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "github.com/veraison/ratsd/attesters/gpu" + "github.com/veraison/ratsd/plugin" +) + +func main() { + plugin.RegisterImplementation(gpu.NewPlugin()) + plugin.Serve() +} diff --git a/go.mod b/go.mod index 09eeed1..0b67979 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/veraison/ratsd go 1.25.0 require ( + github.com/NVIDIA/go-nvml v0.13.0-1 + github.com/confidentsecurity/go-nvtrust v0.2.2 github.com/fxamacker/cbor/v2 v2.7.0 github.com/getkin/kin-openapi v0.131.0 github.com/golang/mock v1.6.0 @@ -11,7 +13,7 @@ require ( github.com/moogar0880/problems v0.1.1 github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/viper v1.13.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/veraison/cmw v0.1.2-0.20250109140511-d907dcce0c61 github.com/veraison/eat v0.0.0-20220117140849-ddaf59d69f53 github.com/veraison/go-cose v1.3.0 diff --git a/go.sum b/go.sum index bf42f1b..cc6c5c7 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NVIDIA/go-nvml v0.13.0-1 h1:OLX8Jq3dONuPOQPC7rndB6+iDmDakw0XTYgzMxObkEw= +github.com/NVIDIA/go-nvml v0.13.0-1/go.mod h1:+KNA7c7gIBH7SKSJ1ntlwkfN80zdx8ovl4hrK3LmPt4= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= @@ -66,6 +68,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/confidentsecurity/go-nvtrust v0.2.2 h1:3IcyLaJggudJQ7lXUWeW8kuWW7ICzAQBeOb0s2XAlaY= +github.com/confidentsecurity/go-nvtrust v0.2.2/go.mod h1:f0C83RCmNBwL0vT8bVZ/+sQYVAbjwLSsw5GuA/AYi2c= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -328,8 +332,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= diff --git a/tokens/gpu-evidence.go b/tokens/gpu-evidence.go new file mode 100644 index 0000000..291e82d --- /dev/null +++ b/tokens/gpu-evidence.go @@ -0,0 +1,90 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package tokens + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/fxamacker/cbor/v2" +) + +const ( + GPUEvidenceMediaTypeCBOR = "application/vnd.veraison.nv-gpu-evidence+cbor" + GPUEvidenceMediaTypeJSON = "application/vnd.veraison.nv-gpu-evidence+json" +) + +type GPUDeviceEvidence struct { + Nonce []byte `json:"nonce"` + Arch string `json:"arch"` + AttestationReport []byte `json:"evidence"` + CertificateChain string `json:"certificate"` +} + +type GPUEvidence struct { + Devices []GPUDeviceEvidence `json:"devices"` +} + +func (g *GPUEvidence) Valid() error { + if len(g.Devices) == 0 { + return errors.New(`missing mandatory field "devices"`) + } + + for i, device := range g.Devices { + if len(device.Nonce) == 0 { + return fmt.Errorf(`missing mandatory field "devices[%d].nonce"`, i) + } + if device.Arch == "" { + return fmt.Errorf(`missing mandatory field "devices[%d].arch"`, i) + } + if len(device.AttestationReport) == 0 { + return fmt.Errorf(`missing mandatory field "devices[%d].evidence"`, i) + } + if device.CertificateChain == "" { + return fmt.Errorf(`missing mandatory field "devices[%d].certificate"`, i) + } + } + + return nil +} + +func (g *GPUEvidence) ToJSON() ([]byte, error) { + if err := g.Valid(); err != nil { + return nil, fmt.Errorf("JSON encoding failed: %w", err) + } + + return json.Marshal(g) +} + +func (g *GPUEvidence) FromJSON(data []byte) error { + if err := json.Unmarshal(data, g); err != nil { + return fmt.Errorf("JSON decoding failed: %w", err) + } + + if err := g.Valid(); err != nil { + return fmt.Errorf("JSON decoding failed: %w", err) + } + + return nil +} + +func (g *GPUEvidence) ToCBOR() ([]byte, error) { + if err := g.Valid(); err != nil { + return nil, fmt.Errorf("CBOR encoding failed: %w", err) + } + + return cbor.Marshal(g) +} + +func (g *GPUEvidence) FromCBOR(data []byte) error { + if err := cbor.Unmarshal(data, g); err != nil { + return fmt.Errorf("CBOR decoding failed: %w", err) + } + + if err := g.Valid(); err != nil { + return fmt.Errorf("CBOR decoding failed: %w", err) + } + + return nil +} diff --git a/tokens/gpu-evidence_test.go b/tokens/gpu-evidence_test.go new file mode 100644 index 0000000..becbfd2 --- /dev/null +++ b/tokens/gpu-evidence_test.go @@ -0,0 +1,77 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package tokens + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + gpuNonce = []byte("12345678901234567890123456789012") + gpuReport = []byte{0xaa, 0xbb, 0xcc, 0xdd} +) + +func validGPUEvidence() *GPUEvidence { + return &GPUEvidence{ + Devices: []GPUDeviceEvidence{ + { + Nonce: gpuNonce, + Arch: "HOPPER", + AttestationReport: gpuReport, + CertificateChain: "certificate-chain", + }, + }, + } +} + +func Test_GPUEvidence_Valid_Pass(t *testing.T) { + assert.NoError(t, validGPUEvidence().Valid()) +} + +func Test_GPUEvidence_Valid_Fail_MissingNonce(t *testing.T) { + evidence := validGPUEvidence() + evidence.Devices[0].Nonce = nil + + assert.EqualError(t, evidence.Valid(), `missing mandatory field "devices[0].nonce"`) +} + +func Test_GPUEvidence_Valid_Fail_MissingDevices(t *testing.T) { + evidence := validGPUEvidence() + evidence.Devices = nil + + assert.EqualError(t, evidence.Valid(), `missing mandatory field "devices"`) +} + +func Test_GPUEvidence_Valid_Fail_MissingCertificateChain(t *testing.T) { + evidence := validGPUEvidence() + evidence.Devices[0].CertificateChain = "" + + assert.EqualError(t, evidence.Valid(), `missing mandatory field "devices[0].certificate"`) +} + +func Test_GPUEvidence_JSON_SerDes_Pass(t *testing.T) { + evidence := validGPUEvidence() + + encodedJSON, err := evidence.ToJSON() + assert.NoError(t, err) + + decodedEvidence := &GPUEvidence{} + assert.NoError(t, decodedEvidence.FromJSON(encodedJSON)) + + assert.True(t, reflect.DeepEqual(evidence, decodedEvidence)) +} + +func Test_GPUEvidence_CBOR_SerDes_Pass(t *testing.T) { + evidence := validGPUEvidence() + + encodedCBOR, err := evidence.ToCBOR() + assert.NoError(t, err) + + decodedEvidence := &GPUEvidence{} + assert.NoError(t, decodedEvidence.FromCBOR(encodedCBOR)) + + assert.True(t, reflect.DeepEqual(evidence, decodedEvidence)) +} From 7f9b7e2062477d0235174562140f0773578eebbd Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Tue, 16 Jun 2026 16:19:35 -0400 Subject: [PATCH 2/8] Align GPU evidence encoding with CDDL Signed-off-by: Ian Chin Wang --- tokens/gpu-evidence.go | 139 ++++++++++++++++++++++++++++++++++-- tokens/gpu-evidence_test.go | 71 ++++++++++++++++-- 2 files changed, 199 insertions(+), 11 deletions(-) diff --git a/tokens/gpu-evidence.go b/tokens/gpu-evidence.go index 291e82d..5cad2db 100644 --- a/tokens/gpu-evidence.go +++ b/tokens/gpu-evidence.go @@ -3,6 +3,8 @@ package tokens import ( + "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -13,6 +15,8 @@ import ( const ( GPUEvidenceMediaTypeCBOR = "application/vnd.veraison.nv-gpu-evidence+cbor" GPUEvidenceMediaTypeJSON = "application/vnd.veraison.nv-gpu-evidence+json" + + gpuEvidenceNonceSize = 32 ) type GPUDeviceEvidence struct { @@ -26,23 +30,43 @@ type GPUEvidence struct { Devices []GPUDeviceEvidence `json:"devices"` } +type gpuDeviceEvidenceWire struct { + Arch string `json:"arch"` + CertificateChain string `json:"certificate"` + AttestationReport string `json:"evidence"` + Nonce string `json:"nonce"` +} + func (g *GPUEvidence) Valid() error { + if g == nil { + return errors.New("nil GPU evidence") + } + if len(g.Devices) == 0 { - return errors.New(`missing mandatory field "devices"`) + return errors.New("missing mandatory GPU evidence device") } for i, device := range g.Devices { if len(device.Nonce) == 0 { - return fmt.Errorf(`missing mandatory field "devices[%d].nonce"`, i) + return fmt.Errorf(`missing mandatory field "[%d].nonce"`, i) + } + if len(device.Nonce) != gpuEvidenceNonceSize { + return fmt.Errorf(`invalid field "[%d].nonce": expected %d bytes, got %d`, i, gpuEvidenceNonceSize, len(device.Nonce)) } if device.Arch == "" { - return fmt.Errorf(`missing mandatory field "devices[%d].arch"`, i) + return fmt.Errorf(`missing mandatory field "[%d].arch"`, i) + } + if device.Arch != "BLACKWELL" && device.Arch != "HOPPER" { + return fmt.Errorf(`invalid field "[%d].arch": expected "BLACKWELL" or "HOPPER", got %q`, i, device.Arch) } if len(device.AttestationReport) == 0 { - return fmt.Errorf(`missing mandatory field "devices[%d].evidence"`, i) + return fmt.Errorf(`missing mandatory field "[%d].evidence"`, i) } if device.CertificateChain == "" { - return fmt.Errorf(`missing mandatory field "devices[%d].certificate"`, i) + return fmt.Errorf(`missing mandatory field "[%d].certificate"`, i) + } + if _, err := base64.StdEncoding.DecodeString(device.CertificateChain); err != nil { + return fmt.Errorf(`invalid field "[%d].certificate": %w`, i, err) } } @@ -88,3 +112,108 @@ func (g *GPUEvidence) FromCBOR(data []byte) error { return nil } + +func (g GPUEvidence) MarshalJSON() ([]byte, error) { + wireDevices, err := g.toWireDevices() + if err != nil { + return nil, err + } + + return json.Marshal(wireDevices) +} + +func (g *GPUEvidence) UnmarshalJSON(data []byte) error { + if g == nil { + return errors.New("nil GPU evidence") + } + + var wireDevices []gpuDeviceEvidenceWire + if err := json.Unmarshal(data, &wireDevices); err != nil { + return err + } + + decoded, err := gpuEvidenceFromWireDevices(wireDevices) + if err != nil { + return err + } + + *g = decoded + return nil +} + +func (g GPUEvidence) MarshalCBOR() ([]byte, error) { + wireDevices, err := g.toWireDevices() + if err != nil { + return nil, err + } + + return cbor.Marshal(wireDevices) +} + +func (g *GPUEvidence) UnmarshalCBOR(data []byte) error { + if g == nil { + return errors.New("nil GPU evidence") + } + + var wireDevices []gpuDeviceEvidenceWire + if err := cbor.Unmarshal(data, &wireDevices); err != nil { + return err + } + + decoded, err := gpuEvidenceFromWireDevices(wireDevices) + if err != nil { + return err + } + + *g = decoded + return nil +} + +func (g GPUEvidence) toWireDevices() ([]gpuDeviceEvidenceWire, error) { + if err := (&g).Valid(); err != nil { + return nil, err + } + + wireDevices := make([]gpuDeviceEvidenceWire, len(g.Devices)) + for i, device := range g.Devices { + wireDevices[i] = gpuDeviceEvidenceWire{ + Arch: device.Arch, + CertificateChain: device.CertificateChain, + AttestationReport: base64.StdEncoding.EncodeToString(device.AttestationReport), + Nonce: hex.EncodeToString(device.Nonce), + } + } + + return wireDevices, nil +} + +func gpuEvidenceFromWireDevices(wireDevices []gpuDeviceEvidenceWire) (GPUEvidence, error) { + evidence := GPUEvidence{ + Devices: make([]GPUDeviceEvidence, len(wireDevices)), + } + + for i, wireDevice := range wireDevices { + nonce, err := hex.DecodeString(wireDevice.Nonce) + if err != nil { + return GPUEvidence{}, fmt.Errorf(`invalid field "[%d].nonce": %w`, i, err) + } + + report, err := base64.StdEncoding.DecodeString(wireDevice.AttestationReport) + if err != nil { + return GPUEvidence{}, fmt.Errorf(`invalid field "[%d].evidence": %w`, i, err) + } + + evidence.Devices[i] = GPUDeviceEvidence{ + Nonce: nonce, + Arch: wireDevice.Arch, + AttestationReport: report, + CertificateChain: wireDevice.CertificateChain, + } + } + + if err := evidence.Valid(); err != nil { + return GPUEvidence{}, err + } + + return evidence, nil +} diff --git a/tokens/gpu-evidence_test.go b/tokens/gpu-evidence_test.go index becbfd2..7655ebd 100644 --- a/tokens/gpu-evidence_test.go +++ b/tokens/gpu-evidence_test.go @@ -3,15 +3,20 @@ package tokens import ( + "encoding/base64" + "encoding/hex" + "encoding/json" "reflect" "testing" + "github.com/fxamacker/cbor/v2" "github.com/stretchr/testify/assert" ) var ( - gpuNonce = []byte("12345678901234567890123456789012") - gpuReport = []byte{0xaa, 0xbb, 0xcc, 0xdd} + gpuNonce = []byte("12345678901234567890123456789012") + gpuReport = []byte{0xaa, 0xbb, 0xcc, 0xdd} + gpuCertificate = base64.StdEncoding.EncodeToString([]byte("certificate-chain")) ) func validGPUEvidence() *GPUEvidence { @@ -21,12 +26,23 @@ func validGPUEvidence() *GPUEvidence { Nonce: gpuNonce, Arch: "HOPPER", AttestationReport: gpuReport, - CertificateChain: "certificate-chain", + CertificateChain: gpuCertificate, }, }, } } +func validGPUWireEvidence() []gpuDeviceEvidenceWire { + return []gpuDeviceEvidenceWire{ + { + Arch: "HOPPER", + CertificateChain: gpuCertificate, + AttestationReport: base64.StdEncoding.EncodeToString(gpuReport), + Nonce: hex.EncodeToString(gpuNonce), + }, + } +} + func Test_GPUEvidence_Valid_Pass(t *testing.T) { assert.NoError(t, validGPUEvidence().Valid()) } @@ -35,21 +51,53 @@ func Test_GPUEvidence_Valid_Fail_MissingNonce(t *testing.T) { evidence := validGPUEvidence() evidence.Devices[0].Nonce = nil - assert.EqualError(t, evidence.Valid(), `missing mandatory field "devices[0].nonce"`) + assert.EqualError(t, evidence.Valid(), `missing mandatory field "[0].nonce"`) +} + +func Test_GPUEvidence_Valid_Fail_WrongNonceSize(t *testing.T) { + evidence := validGPUEvidence() + evidence.Devices[0].Nonce = []byte("short") + + assert.EqualError(t, evidence.Valid(), `invalid field "[0].nonce": expected 32 bytes, got 5`) } func Test_GPUEvidence_Valid_Fail_MissingDevices(t *testing.T) { evidence := validGPUEvidence() evidence.Devices = nil - assert.EqualError(t, evidence.Valid(), `missing mandatory field "devices"`) + assert.EqualError(t, evidence.Valid(), "missing mandatory GPU evidence device") +} + +func Test_GPUEvidence_Valid_Fail_InvalidArch(t *testing.T) { + evidence := validGPUEvidence() + evidence.Devices[0].Arch = "AMPERE" + + assert.EqualError(t, evidence.Valid(), `invalid field "[0].arch": expected "BLACKWELL" or "HOPPER", got "AMPERE"`) } func Test_GPUEvidence_Valid_Fail_MissingCertificateChain(t *testing.T) { evidence := validGPUEvidence() evidence.Devices[0].CertificateChain = "" - assert.EqualError(t, evidence.Valid(), `missing mandatory field "devices[0].certificate"`) + assert.EqualError(t, evidence.Valid(), `missing mandatory field "[0].certificate"`) +} + +func Test_GPUEvidence_Valid_Fail_InvalidCertificateChain(t *testing.T) { + evidence := validGPUEvidence() + evidence.Devices[0].CertificateChain = "%" + + assert.ErrorContains(t, evidence.Valid(), `invalid field "[0].certificate"`) +} + +func Test_GPUEvidence_JSON_WireShape(t *testing.T) { + evidence := validGPUEvidence() + + encodedJSON, err := evidence.ToJSON() + assert.NoError(t, err) + + var wire []gpuDeviceEvidenceWire + assert.NoError(t, json.Unmarshal(encodedJSON, &wire)) + assert.Equal(t, validGPUWireEvidence(), wire) } func Test_GPUEvidence_JSON_SerDes_Pass(t *testing.T) { @@ -64,6 +112,17 @@ func Test_GPUEvidence_JSON_SerDes_Pass(t *testing.T) { assert.True(t, reflect.DeepEqual(evidence, decodedEvidence)) } +func Test_GPUEvidence_CBOR_WireShape(t *testing.T) { + evidence := validGPUEvidence() + + encodedCBOR, err := evidence.ToCBOR() + assert.NoError(t, err) + + var wire []gpuDeviceEvidenceWire + assert.NoError(t, cbor.Unmarshal(encodedCBOR, &wire)) + assert.Equal(t, validGPUWireEvidence(), wire) +} + func Test_GPUEvidence_CBOR_SerDes_Pass(t *testing.T) { evidence := validGPUEvidence() From 9c6f7e72f4c6e1c54abacd2a61441cb953e9442f Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Tue, 7 Jul 2026 10:49:25 -0400 Subject: [PATCH 3/8] Update Nvidia GPU evidence media type Signed-off-by: Ian Chin Wang --- tokens/gpu-evidence.go | 4 ++-- tokens/gpu-evidence_test.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tokens/gpu-evidence.go b/tokens/gpu-evidence.go index 5cad2db..b398ab9 100644 --- a/tokens/gpu-evidence.go +++ b/tokens/gpu-evidence.go @@ -13,8 +13,8 @@ import ( ) const ( - GPUEvidenceMediaTypeCBOR = "application/vnd.veraison.nv-gpu-evidence+cbor" - GPUEvidenceMediaTypeJSON = "application/vnd.veraison.nv-gpu-evidence+json" + GPUEvidenceMediaTypeCBOR = "application/vnd.veraison.nvidia-gpu-evidence+cbor" + GPUEvidenceMediaTypeJSON = "application/vnd.veraison.nvidia-gpu-evidence+json" gpuEvidenceNonceSize = 32 ) diff --git a/tokens/gpu-evidence_test.go b/tokens/gpu-evidence_test.go index 7655ebd..7005e7b 100644 --- a/tokens/gpu-evidence_test.go +++ b/tokens/gpu-evidence_test.go @@ -43,6 +43,11 @@ func validGPUWireEvidence() []gpuDeviceEvidenceWire { } } +func Test_GPUEvidence_MediaTypes(t *testing.T) { + assert.Equal(t, "application/vnd.veraison.nvidia-gpu-evidence+cbor", GPUEvidenceMediaTypeCBOR) + assert.Equal(t, "application/vnd.veraison.nvidia-gpu-evidence+json", GPUEvidenceMediaTypeJSON) +} + func Test_GPUEvidence_Valid_Pass(t *testing.T) { assert.NoError(t, validGPUEvidence().Valid()) } From 2ea0941d8b301377d532db333622634ebbb88555 Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Tue, 7 Jul 2026 10:57:17 -0400 Subject: [PATCH 4/8] Remove Nvidia GPU evidence CBOR media type Signed-off-by: Ian Chin Wang --- attesters/gpu/gpu.go | 11 -------- attesters/gpu/gpu_test.go | 51 ++++++++++++------------------------- tokens/gpu-evidence.go | 1 - tokens/gpu-evidence_test.go | 1 - 4 files changed, 16 insertions(+), 48 deletions(-) diff --git a/attesters/gpu/gpu.go b/attesters/gpu/gpu.go index 458f342..8061a61 100644 --- a/attesters/gpu/gpu.go +++ b/attesters/gpu/gpu.go @@ -14,7 +14,6 @@ import ( const ( ApplicationvndVeraisonNvGpuEvidenceJSON = tokens.GPUEvidenceMediaTypeJSON - ApplicationvndVeraisonNvGpuEvidenceCBOR = tokens.GPUEvidenceMediaTypeCBOR gpuNonceSize = nvml.CC_GPU_CEC_NONCE_SIZE ) @@ -29,10 +28,6 @@ var ( ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, NonceSize: gpuNonceSize, }, - { - ContentType: ApplicationvndVeraisonNvGpuEvidenceCBOR, - NonceSize: gpuNonceSize, - }, } statusSucceeded = &compositor.Status{Result: true, Error: ""} @@ -202,12 +197,6 @@ func encodeEvidence(contentType string, nonce []byte, devices []nvgpu.GPUDevice) return nil, fmt.Errorf("failed to JSON encode GPU evidence: %v", err) } return encodedEvidence, nil - case ApplicationvndVeraisonNvGpuEvidenceCBOR: - encodedEvidence, err := token.ToCBOR() - if err != nil { - return nil, fmt.Errorf("failed to CBOR encode GPU evidence: %v", err) - } - return encodedEvidence, nil default: return nil, fmt.Errorf("no supported format in gpu plugin matches the requested format") } diff --git a/attesters/gpu/gpu_test.go b/attesters/gpu/gpu_test.go index dae6d51..e5a458b 100644 --- a/attesters/gpu/gpu_test.go +++ b/attesters/gpu/gpu_test.go @@ -142,6 +142,22 @@ func Test_GetEvidence_InvalidFormat(t *testing.T) { assert.Equal(t, expected, NewPlugin().GetEvidence(in)) } +func Test_GetEvidence_CBORMediaTypeUnsupported(t *testing.T) { + in := &compositor.EvidenceIn{ + ContentType: "application/vnd.veraison.nvidia-gpu-evidence+cbor", + Nonce: []byte("12345678901234567890123456789012"), + } + + expected := &compositor.EvidenceOut{ + Status: &compositor.Status{ + Result: false, + Error: "no supported format in gpu plugin matches the requested format", + }, + } + + assert.Equal(t, expected, NewPlugin().GetEvidence(in)) +} + func Test_GetEvidence_InvalidOptions(t *testing.T) { tests := []struct { name string @@ -241,41 +257,6 @@ func Test_GetEvidence_JSON(t *testing.T) { assert.True(t, collector.shutdownInvoked) } -func Test_GetEvidence_CBOR(t *testing.T) { - collector := &fakeCollector{ - devices: validGPUDevices(t), - } - p := makePlugin(func() (evidenceCollector, error) { - return collector, nil - }) - - nonce := []byte("12345678901234567890123456789012") - in := &compositor.EvidenceIn{ - ContentType: ApplicationvndVeraisonNvGpuEvidenceCBOR, - Nonce: nonce, - } - - expectedToken := &tokens.GPUEvidence{ - Devices: []tokens.GPUDeviceEvidence{ - { - Nonce: nonce, - Arch: "HOPPER", - AttestationReport: []byte("attestation-report"), - CertificateChain: mustCertChainBase64(t), - }, - }, - } - expectedEvidence, err := expectedToken.ToCBOR() - assert.NoError(t, err) - - expected := &compositor.EvidenceOut{ - Status: statusSucceeded, - Evidence: expectedEvidence, - } - - assert.Equal(t, expected, p.GetEvidence(in)) -} - func Test_GetEvidence_ShutdownFailure(t *testing.T) { collector := &fakeCollector{ devices: validGPUDevices(t), diff --git a/tokens/gpu-evidence.go b/tokens/gpu-evidence.go index b398ab9..220aba0 100644 --- a/tokens/gpu-evidence.go +++ b/tokens/gpu-evidence.go @@ -13,7 +13,6 @@ import ( ) const ( - GPUEvidenceMediaTypeCBOR = "application/vnd.veraison.nvidia-gpu-evidence+cbor" GPUEvidenceMediaTypeJSON = "application/vnd.veraison.nvidia-gpu-evidence+json" gpuEvidenceNonceSize = 32 diff --git a/tokens/gpu-evidence_test.go b/tokens/gpu-evidence_test.go index 7005e7b..83ddb3c 100644 --- a/tokens/gpu-evidence_test.go +++ b/tokens/gpu-evidence_test.go @@ -44,7 +44,6 @@ func validGPUWireEvidence() []gpuDeviceEvidenceWire { } func Test_GPUEvidence_MediaTypes(t *testing.T) { - assert.Equal(t, "application/vnd.veraison.nvidia-gpu-evidence+cbor", GPUEvidenceMediaTypeCBOR) assert.Equal(t, "application/vnd.veraison.nvidia-gpu-evidence+json", GPUEvidenceMediaTypeJSON) } From 02899ba3fce0a2ca776b4fcab1ce127f9f59a632 Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Tue, 7 Jul 2026 13:52:58 -0400 Subject: [PATCH 5/8] Remove GPU evidence CBOR helpers Signed-off-by: Ian Chin Wang --- tokens/gpu-evidence.go | 20 -------------------- tokens/gpu-evidence_test.go | 6 +++--- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/tokens/gpu-evidence.go b/tokens/gpu-evidence.go index 220aba0..f8c63d1 100644 --- a/tokens/gpu-evidence.go +++ b/tokens/gpu-evidence.go @@ -92,26 +92,6 @@ func (g *GPUEvidence) FromJSON(data []byte) error { return nil } -func (g *GPUEvidence) ToCBOR() ([]byte, error) { - if err := g.Valid(); err != nil { - return nil, fmt.Errorf("CBOR encoding failed: %w", err) - } - - return cbor.Marshal(g) -} - -func (g *GPUEvidence) FromCBOR(data []byte) error { - if err := cbor.Unmarshal(data, g); err != nil { - return fmt.Errorf("CBOR decoding failed: %w", err) - } - - if err := g.Valid(); err != nil { - return fmt.Errorf("CBOR decoding failed: %w", err) - } - - return nil -} - func (g GPUEvidence) MarshalJSON() ([]byte, error) { wireDevices, err := g.toWireDevices() if err != nil { diff --git a/tokens/gpu-evidence_test.go b/tokens/gpu-evidence_test.go index 83ddb3c..3f93f2b 100644 --- a/tokens/gpu-evidence_test.go +++ b/tokens/gpu-evidence_test.go @@ -119,7 +119,7 @@ func Test_GPUEvidence_JSON_SerDes_Pass(t *testing.T) { func Test_GPUEvidence_CBOR_WireShape(t *testing.T) { evidence := validGPUEvidence() - encodedCBOR, err := evidence.ToCBOR() + encodedCBOR, err := cbor.Marshal(evidence) assert.NoError(t, err) var wire []gpuDeviceEvidenceWire @@ -130,11 +130,11 @@ func Test_GPUEvidence_CBOR_WireShape(t *testing.T) { func Test_GPUEvidence_CBOR_SerDes_Pass(t *testing.T) { evidence := validGPUEvidence() - encodedCBOR, err := evidence.ToCBOR() + encodedCBOR, err := cbor.Marshal(evidence) assert.NoError(t, err) decodedEvidence := &GPUEvidence{} - assert.NoError(t, decodedEvidence.FromCBOR(encodedCBOR)) + assert.NoError(t, cbor.Unmarshal(encodedCBOR, decodedEvidence)) assert.True(t, reflect.DeepEqual(evidence, decodedEvidence)) } From d969280b20703d242a87f0aa708285c3208840eb Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Tue, 21 Jul 2026 17:19:10 -0400 Subject: [PATCH 6/8] Address NVIDIA GPU attester review feedback Signed-off-by: Ian Chin Wang --- README.md | 5 + attesters/Makefile | 2 +- attesters/{gpu => nvgpu}/Makefile | 2 +- attesters/{gpu/gpu.go => nvgpu/nvgpu.go} | 79 +++++----- .../{gpu/gpu_test.go => nvgpu/nvgpu_test.go} | 66 ++++++--- attesters/{gpu => nvgpu}/plugin/Makefile | 4 +- attesters/{gpu => nvgpu}/plugin/main.go | 4 +- docs/nvidia-gpu-evidence.cddl | 2 +- tokens/gpu-evidence.go | 135 ++---------------- tokens/gpu-evidence_test.go | 64 ++++----- 10 files changed, 140 insertions(+), 223 deletions(-) rename attesters/{gpu => nvgpu}/Makefile (78%) rename attesters/{gpu/gpu.go => nvgpu/nvgpu.go} (64%) rename attesters/{gpu/gpu_test.go => nvgpu/nvgpu_test.go} (76%) rename attesters/{gpu => nvgpu}/plugin/Makefile (62%) rename attesters/{gpu => nvgpu}/plugin/main.go (66%) diff --git a/README.md b/README.md index 909adc1..04737df 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,11 @@ make[3]: Leaving directory '/builddir/build/BUILD/ratsd-1.0.3+la3/attesters/mock make[2]: Leaving directory '/builddir/build/BUILD/ratsd-1.0.3+la3/attesters/mocktsm' make[1]: Leaving directory '/builddir/build/BUILD/ratsd-1.0.3+la3/attesters' ``` + +### NVIDIA GPU attester prerequisites + +The `nvgpu` attester supports NVIDIA Hopper and Blackwell GPUs with confidential computing enabled. The attester host must have a compatible NVIDIA driver installed; the driver provides the `libnvidia-ml.so.1` NVML library that the plugin loads at runtime. Containerized deployments must expose the NVIDIA devices and driver libraries to the container. + # Query ratsd By default, ratsd core listens on port 8895. Use `POST /ratsd/chares` to retrieve a CMW collection containing evidence from each sub-attester. This API call requires the request body to be the JSON object `{"nonce": $(Base64 string of 64-byte data)}` replacing the placeholder with a proper base64 string. See the following example: diff --git a/attesters/Makefile b/attesters/Makefile index 47e6292..0e371c5 100644 --- a/attesters/Makefile +++ b/attesters/Makefile @@ -3,7 +3,7 @@ SUBDIR := tsm SUBDIR += mocktsm -SUBDIR += gpu +SUBDIR += nvgpu clean: ; $(RM) -rf ./bin diff --git a/attesters/gpu/Makefile b/attesters/nvgpu/Makefile similarity index 78% rename from attesters/gpu/Makefile rename to attesters/nvgpu/Makefile index d536fe6..7d2b92e 100644 --- a/attesters/gpu/Makefile +++ b/attesters/nvgpu/Makefile @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 .DEFAULT_GOAL := test -GOPKG := github.com/veraison/ratsd/attesters/gpu +GOPKG := github.com/veraison/ratsd/attesters/nvgpu SRCS := $(wildcard *.go) SUBDIR += plugin diff --git a/attesters/gpu/gpu.go b/attesters/nvgpu/nvgpu.go similarity index 64% rename from attesters/gpu/gpu.go rename to attesters/nvgpu/nvgpu.go index 8061a61..aa3ef42 100644 --- a/attesters/gpu/gpu.go +++ b/attesters/nvgpu/nvgpu.go @@ -1,20 +1,20 @@ // Copyright 2026 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 -package gpu +package nvgpu import ( "encoding/json" "fmt" "github.com/NVIDIA/go-nvml/pkg/nvml" - nvgpu "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/gpu" + nvtrustgpu "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/gpu" "github.com/veraison/ratsd/proto/compositor" "github.com/veraison/ratsd/tokens" ) const ( ApplicationvndVeraisonNvGpuEvidenceJSON = tokens.GPUEvidenceMediaTypeJSON - gpuNonceSize = nvml.CC_GPU_CEC_NONCE_SIZE + nonceSize = nvml.CC_GPU_CEC_NONCE_SIZE ) var ( @@ -26,7 +26,7 @@ var ( supportedFormats = []*compositor.Format{ { ContentType: ApplicationvndVeraisonNvGpuEvidenceJSON, - NonceSize: gpuNonceSize, + NonceSize: nonceSize, }, } @@ -34,22 +34,43 @@ var ( ) type evidenceCollector interface { - CollectEvidence(nonce []byte) ([]nvgpu.GPUDevice, error) + CollectEvidence(nonce []byte) ([]nvtrustgpu.GPUDevice, error) Shutdown() error } type collectorFactory func() (evidenceCollector, error) -type GPUPlugin struct { - newCollector collectorFactory +type Plugin struct { + newCollector collectorFactory + availabilityErr error } -func NewPlugin() *GPUPlugin { - return &GPUPlugin{newCollector: defaultCollectorFactory} +func NewPlugin() *Plugin { + return newPlugin(defaultCollectorFactory) +} + +func newPlugin(factory collectorFactory) *Plugin { + p := &Plugin{newCollector: factory} + p.availabilityErr = p.initialize() + return p } func defaultCollectorFactory() (evidenceCollector, error) { - return nvgpu.NewNvmlGPUAdmin(nil) + return nvtrustgpu.NewNvmlGPUAdmin(nil) +} + +// initialize probes the NVIDIA driver once when the plugin starts. Creating a +// collector loads and initializes NVML; shutting it down immediately leaves no +// resources open until evidence collection is requested. The plugin interface +// has no initialization hook, so GetSupportedFormats reports this probe's result +// and does not advertise a format that the host cannot produce. +func (p *Plugin) initialize() error { + collector, err := p.newCollector() + if err != nil { + return err + } + + return collector.Shutdown() } func getEvidenceError(e error) *compositor.EvidenceOut { @@ -61,36 +82,26 @@ func getEvidenceError(e error) *compositor.EvidenceOut { } } -func (g *GPUPlugin) GetOptions() *compositor.OptionsOut { +func (p *Plugin) GetOptions() *compositor.OptionsOut { return &compositor.OptionsOut{ Options: []*compositor.Option{}, Status: statusSucceeded, } } -func (g *GPUPlugin) GetSubAttesterID() *compositor.SubAttesterIDOut { +func (p *Plugin) GetSubAttesterID() *compositor.SubAttesterIDOut { return &compositor.SubAttesterIDOut{ SubAttesterID: sid, Status: statusSucceeded, } } -func (g *GPUPlugin) GetSupportedFormats() *compositor.SupportedFormatsOut { - collector, err := g.newCollector() - if err != nil { - return &compositor.SupportedFormatsOut{ - Status: &compositor.Status{ - Result: false, - Error: fmt.Sprintf("GPU evidence collection is not available: %s", err.Error()), - }, - } - } - - if err := collector.Shutdown(); err != nil { +func (p *Plugin) GetSupportedFormats() *compositor.SupportedFormatsOut { + if p.availabilityErr != nil { return &compositor.SupportedFormatsOut{ Status: &compositor.Status{ Result: false, - Error: fmt.Sprintf("GPU evidence collection is not available: %s", err.Error()), + Error: fmt.Sprintf("NVIDIA GPU evidence collection is not available: %s", p.availabilityErr), }, } } @@ -101,11 +112,11 @@ func (g *GPUPlugin) GetSupportedFormats() *compositor.SupportedFormatsOut { } } -func (g *GPUPlugin) GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut { - if uint32(len(in.Nonce)) != gpuNonceSize { +func (p *Plugin) GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut { + if uint32(len(in.Nonce)) != nonceSize { errMsg := fmt.Errorf( - "nonce size of the GPU attester should be %d, got %d", - gpuNonceSize, uint32(len(in.Nonce)), + "nonce size of the NVIDIA GPU attester should be %d, got %d", + nonceSize, uint32(len(in.Nonce)), ) return getEvidenceError(errMsg) } @@ -115,10 +126,10 @@ func (g *GPUPlugin) GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceO } if !supportsFormat(in.ContentType) { - return getEvidenceError(fmt.Errorf("no supported format in gpu plugin matches the requested format")) + return getEvidenceError(fmt.Errorf("no supported format in nvgpu plugin matches the requested format")) } - collector, err := g.newCollector() + collector, err := p.newCollector() if err != nil { return getEvidenceError(fmt.Errorf("failed to initialize GPU evidence collector: %v", err)) } @@ -165,13 +176,13 @@ func validateOptions(options []byte) error { } if len(parsed) > 0 { - return fmt.Errorf("gpu attester does not support options") + return fmt.Errorf("NVIDIA GPU attester does not support options") } return nil } -func encodeEvidence(contentType string, nonce []byte, devices []nvgpu.GPUDevice) ([]byte, error) { +func encodeEvidence(contentType string, nonce []byte, devices []nvtrustgpu.GPUDevice) ([]byte, error) { token := &tokens.GPUEvidence{ Devices: make([]tokens.GPUDeviceEvidence, len(devices)), } @@ -198,6 +209,6 @@ func encodeEvidence(contentType string, nonce []byte, devices []nvgpu.GPUDevice) } return encodedEvidence, nil default: - return nil, fmt.Errorf("no supported format in gpu plugin matches the requested format") + return nil, fmt.Errorf("no supported format in nvgpu plugin matches the requested format") } } diff --git a/attesters/gpu/gpu_test.go b/attesters/nvgpu/nvgpu_test.go similarity index 76% rename from attesters/gpu/gpu_test.go rename to attesters/nvgpu/nvgpu_test.go index e5a458b..532aed0 100644 --- a/attesters/gpu/gpu_test.go +++ b/attesters/nvgpu/nvgpu_test.go @@ -1,6 +1,6 @@ // Copyright 2026 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 -package gpu +package nvgpu import ( "errors" @@ -9,7 +9,7 @@ import ( "github.com/NVIDIA/go-nvml/pkg/nvml" "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/certs" - nvgpu "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/gpu" + nvtrustgpu "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/gpu" nvmocks "github.com/confidentsecurity/go-nvtrust/pkg/gonvtrust/mocks" "github.com/stretchr/testify/assert" "github.com/veraison/ratsd/proto/compositor" @@ -17,14 +17,14 @@ import ( ) type fakeCollector struct { - devices []nvgpu.GPUDevice + devices []nvtrustgpu.GPUDevice collectErr error shutdownErr error collectedNonce []byte shutdownInvoked bool } -func (f *fakeCollector) CollectEvidence(nonce []byte) ([]nvgpu.GPUDevice, error) { +func (f *fakeCollector) CollectEvidence(nonce []byte) ([]nvtrustgpu.GPUDevice, error) { f.collectedNonce = append([]byte(nil), nonce...) if f.collectErr != nil { return nil, f.collectErr @@ -38,19 +38,25 @@ func (f *fakeCollector) Shutdown() error { return f.shutdownErr } -func makePlugin(factory collectorFactory) *GPUPlugin { - return &GPUPlugin{newCollector: factory} +func makePlugin(factory collectorFactory) *Plugin { + return newPlugin(factory) } -func validGPUDevices(t *testing.T) []nvgpu.GPUDevice { +func availablePlugin() *Plugin { + return makePlugin(func() (evidenceCollector, error) { + return &fakeCollector{}, nil + }) +} + +func validGPUDevices(t *testing.T) []nvtrustgpu.GPUDevice { t.Helper() certChain := certs.NewCertChainFromData(nvmocks.ValidCertChainData) requireErr := certChain.Verify() assert.NoError(t, requireErr) - return []nvgpu.GPUDevice{ - nvgpu.NewGPUDevice( + return []nvtrustgpu.GPUDevice{ + nvtrustgpu.NewGPUDevice( nvml.DEVICE_ARCH_HOPPER, []byte("attestation-report"), certChain, @@ -64,7 +70,7 @@ func Test_GetOptions(t *testing.T) { Status: statusSucceeded, } - assert.Equal(t, expected, NewPlugin().GetOptions()) + assert.Equal(t, expected, availablePlugin().GetOptions()) } func Test_GetSubAttesterID(t *testing.T) { @@ -73,12 +79,14 @@ func Test_GetSubAttesterID(t *testing.T) { Status: statusSucceeded, } - assert.Equal(t, expected, NewPlugin().GetSubAttesterID()) + assert.Equal(t, expected, availablePlugin().GetSubAttesterID()) } func Test_GetSupportedFormats(t *testing.T) { collector := &fakeCollector{} + factoryCalls := 0 p := makePlugin(func() (evidenceCollector, error) { + factoryCalls++ return collector, nil }) @@ -88,6 +96,7 @@ func Test_GetSupportedFormats(t *testing.T) { } assert.Equal(t, expected, p.GetSupportedFormats()) + assert.Equal(t, 1, factoryCalls) assert.True(t, collector.shutdownInvoked) } @@ -99,7 +108,22 @@ func Test_GetSupportedFormats_InitFailure(t *testing.T) { expected := &compositor.SupportedFormatsOut{ Status: &compositor.Status{ Result: false, - Error: "GPU evidence collection is not available: nvml unavailable", + Error: "NVIDIA GPU evidence collection is not available: nvml unavailable", + }, + } + + assert.Equal(t, expected, p.GetSupportedFormats()) +} + +func Test_GetSupportedFormats_ShutdownFailure(t *testing.T) { + p := makePlugin(func() (evidenceCollector, error) { + return &fakeCollector{shutdownErr: errors.New("shutdown failed")}, nil + }) + + expected := &compositor.SupportedFormatsOut{ + Status: &compositor.Status{ + Result: false, + Error: "NVIDIA GPU evidence collection is not available: shutdown failed", }, } @@ -113,8 +137,8 @@ func Test_GetEvidence_WrongNonceSize(t *testing.T) { } errMsg := fmt.Sprintf( - "nonce size of the GPU attester should be %d, got %d", - gpuNonceSize, len(in.Nonce), + "nonce size of the NVIDIA GPU attester should be %d, got %d", + nonceSize, len(in.Nonce), ) expected := &compositor.EvidenceOut{ Status: &compositor.Status{ @@ -123,7 +147,7 @@ func Test_GetEvidence_WrongNonceSize(t *testing.T) { }, } - assert.Equal(t, expected, NewPlugin().GetEvidence(in)) + assert.Equal(t, expected, availablePlugin().GetEvidence(in)) } func Test_GetEvidence_InvalidFormat(t *testing.T) { @@ -135,11 +159,11 @@ func Test_GetEvidence_InvalidFormat(t *testing.T) { expected := &compositor.EvidenceOut{ Status: &compositor.Status{ Result: false, - Error: "no supported format in gpu plugin matches the requested format", + Error: "no supported format in nvgpu plugin matches the requested format", }, } - assert.Equal(t, expected, NewPlugin().GetEvidence(in)) + assert.Equal(t, expected, availablePlugin().GetEvidence(in)) } func Test_GetEvidence_CBORMediaTypeUnsupported(t *testing.T) { @@ -151,11 +175,11 @@ func Test_GetEvidence_CBORMediaTypeUnsupported(t *testing.T) { expected := &compositor.EvidenceOut{ Status: &compositor.Status{ Result: false, - Error: "no supported format in gpu plugin matches the requested format", + Error: "no supported format in nvgpu plugin matches the requested format", }, } - assert.Equal(t, expected, NewPlugin().GetEvidence(in)) + assert.Equal(t, expected, availablePlugin().GetEvidence(in)) } func Test_GetEvidence_InvalidOptions(t *testing.T) { @@ -172,7 +196,7 @@ func Test_GetEvidence_InvalidOptions(t *testing.T) { { name: "unsupported option", opts: `{"mode":"full"}`, - msg: "gpu attester does not support options", + msg: "NVIDIA GPU attester does not support options", }, } @@ -191,7 +215,7 @@ func Test_GetEvidence_InvalidOptions(t *testing.T) { }, } - assert.Equal(t, expected, NewPlugin().GetEvidence(in)) + assert.Equal(t, expected, availablePlugin().GetEvidence(in)) }) } } diff --git a/attesters/gpu/plugin/Makefile b/attesters/nvgpu/plugin/Makefile similarity index 62% rename from attesters/gpu/plugin/Makefile rename to attesters/nvgpu/plugin/Makefile index c6d8aaa..38ab038 100644 --- a/attesters/gpu/plugin/Makefile +++ b/attesters/nvgpu/plugin/Makefile @@ -1,8 +1,8 @@ # Copyright 2026 Contributors to the Veraison project. # SPDX-License-Identifier: Apache-2.0 -PLUGIN := ../../bin/gpu.plugin -GOPKG := github.com/veraison/ratsd/attesters/gpu +PLUGIN := ../../bin/nvgpu.plugin +GOPKG := github.com/veraison/ratsd/attesters/nvgpu SRCS := main.go include ../../../mk/plugin.mk diff --git a/attesters/gpu/plugin/main.go b/attesters/nvgpu/plugin/main.go similarity index 66% rename from attesters/gpu/plugin/main.go rename to attesters/nvgpu/plugin/main.go index 83f800a..74e58a7 100644 --- a/attesters/gpu/plugin/main.go +++ b/attesters/nvgpu/plugin/main.go @@ -3,11 +3,11 @@ package main import ( - "github.com/veraison/ratsd/attesters/gpu" + "github.com/veraison/ratsd/attesters/nvgpu" "github.com/veraison/ratsd/plugin" ) func main() { - plugin.RegisterImplementation(gpu.NewPlugin()) + plugin.RegisterImplementation(nvgpu.NewPlugin()) plugin.Serve() } diff --git a/docs/nvidia-gpu-evidence.cddl b/docs/nvidia-gpu-evidence.cddl index 5a1d0c3..ec1d21b 100644 --- a/docs/nvidia-gpu-evidence.cddl +++ b/docs/nvidia-gpu-evidence.cddl @@ -10,4 +10,4 @@ nvidia-gpu-evidence = [ arch-type = "BLACKWELL" / "HOPPER" certificate-type = text .b64c bytes evidence-type = text .b64c bytes -nonce-type = text .hex (bytes .size 32) +nonce-type = text .b64c (bytes .size 32) diff --git a/tokens/gpu-evidence.go b/tokens/gpu-evidence.go index f8c63d1..0e75da4 100644 --- a/tokens/gpu-evidence.go +++ b/tokens/gpu-evidence.go @@ -4,18 +4,17 @@ package tokens import ( "encoding/base64" - "encoding/hex" "encoding/json" "errors" "fmt" - "github.com/fxamacker/cbor/v2" + "github.com/NVIDIA/go-nvml/pkg/nvml" ) const ( GPUEvidenceMediaTypeJSON = "application/vnd.veraison.nvidia-gpu-evidence+json" - gpuEvidenceNonceSize = 32 + gpuEvidenceNonceSize = nvml.CC_GPU_CEC_NONCE_SIZE ) type GPUDeviceEvidence struct { @@ -29,18 +28,7 @@ type GPUEvidence struct { Devices []GPUDeviceEvidence `json:"devices"` } -type gpuDeviceEvidenceWire struct { - Arch string `json:"arch"` - CertificateChain string `json:"certificate"` - AttestationReport string `json:"evidence"` - Nonce string `json:"nonce"` -} - -func (g *GPUEvidence) Valid() error { - if g == nil { - return errors.New("nil GPU evidence") - } - +func (g GPUEvidence) Valid() error { if len(g.Devices) == 0 { return errors.New("missing mandatory GPU evidence device") } @@ -73,126 +61,29 @@ func (g *GPUEvidence) Valid() error { } func (g *GPUEvidence) ToJSON() ([]byte, error) { - if err := g.Valid(); err != nil { - return nil, fmt.Errorf("JSON encoding failed: %w", err) - } - - return json.Marshal(g) -} - -func (g *GPUEvidence) FromJSON(data []byte) error { - if err := json.Unmarshal(data, g); err != nil { - return fmt.Errorf("JSON decoding failed: %w", err) - } - - if err := g.Valid(); err != nil { - return fmt.Errorf("JSON decoding failed: %w", err) - } - - return nil -} - -func (g GPUEvidence) MarshalJSON() ([]byte, error) { - wireDevices, err := g.toWireDevices() - if err != nil { - return nil, err - } - - return json.Marshal(wireDevices) -} - -func (g *GPUEvidence) UnmarshalJSON(data []byte) error { if g == nil { - return errors.New("nil GPU evidence") - } - - var wireDevices []gpuDeviceEvidenceWire - if err := json.Unmarshal(data, &wireDevices); err != nil { - return err - } - - decoded, err := gpuEvidenceFromWireDevices(wireDevices) - if err != nil { - return err + return nil, errors.New("JSON encoding failed: nil GPU evidence") } - *g = decoded - return nil -} - -func (g GPUEvidence) MarshalCBOR() ([]byte, error) { - wireDevices, err := g.toWireDevices() - if err != nil { - return nil, err + if err := g.Valid(); err != nil { + return nil, fmt.Errorf("JSON encoding failed: %w", err) } - return cbor.Marshal(wireDevices) + return json.Marshal(g.Devices) } -func (g *GPUEvidence) UnmarshalCBOR(data []byte) error { +func (g *GPUEvidence) FromJSON(data []byte) error { if g == nil { - return errors.New("nil GPU evidence") + return errors.New("JSON decoding failed: nil GPU evidence") } - var wireDevices []gpuDeviceEvidenceWire - if err := cbor.Unmarshal(data, &wireDevices); err != nil { - return err + if err := json.Unmarshal(data, &g.Devices); err != nil { + return fmt.Errorf("JSON decoding failed: %w", err) } - decoded, err := gpuEvidenceFromWireDevices(wireDevices) - if err != nil { - return err + if err := g.Valid(); err != nil { + return fmt.Errorf("JSON decoding failed: %w", err) } - *g = decoded return nil } - -func (g GPUEvidence) toWireDevices() ([]gpuDeviceEvidenceWire, error) { - if err := (&g).Valid(); err != nil { - return nil, err - } - - wireDevices := make([]gpuDeviceEvidenceWire, len(g.Devices)) - for i, device := range g.Devices { - wireDevices[i] = gpuDeviceEvidenceWire{ - Arch: device.Arch, - CertificateChain: device.CertificateChain, - AttestationReport: base64.StdEncoding.EncodeToString(device.AttestationReport), - Nonce: hex.EncodeToString(device.Nonce), - } - } - - return wireDevices, nil -} - -func gpuEvidenceFromWireDevices(wireDevices []gpuDeviceEvidenceWire) (GPUEvidence, error) { - evidence := GPUEvidence{ - Devices: make([]GPUDeviceEvidence, len(wireDevices)), - } - - for i, wireDevice := range wireDevices { - nonce, err := hex.DecodeString(wireDevice.Nonce) - if err != nil { - return GPUEvidence{}, fmt.Errorf(`invalid field "[%d].nonce": %w`, i, err) - } - - report, err := base64.StdEncoding.DecodeString(wireDevice.AttestationReport) - if err != nil { - return GPUEvidence{}, fmt.Errorf(`invalid field "[%d].evidence": %w`, i, err) - } - - evidence.Devices[i] = GPUDeviceEvidence{ - Nonce: nonce, - Arch: wireDevice.Arch, - AttestationReport: report, - CertificateChain: wireDevice.CertificateChain, - } - } - - if err := evidence.Valid(); err != nil { - return GPUEvidence{}, err - } - - return evidence, nil -} diff --git a/tokens/gpu-evidence_test.go b/tokens/gpu-evidence_test.go index 3f93f2b..7b3b82e 100644 --- a/tokens/gpu-evidence_test.go +++ b/tokens/gpu-evidence_test.go @@ -4,12 +4,12 @@ package tokens import ( "encoding/base64" - "encoding/hex" "encoding/json" + "fmt" "reflect" "testing" - "github.com/fxamacker/cbor/v2" + "github.com/NVIDIA/go-nvml/pkg/nvml" "github.com/stretchr/testify/assert" ) @@ -32,17 +32,6 @@ func validGPUEvidence() *GPUEvidence { } } -func validGPUWireEvidence() []gpuDeviceEvidenceWire { - return []gpuDeviceEvidenceWire{ - { - Arch: "HOPPER", - CertificateChain: gpuCertificate, - AttestationReport: base64.StdEncoding.EncodeToString(gpuReport), - Nonce: hex.EncodeToString(gpuNonce), - }, - } -} - func Test_GPUEvidence_MediaTypes(t *testing.T) { assert.Equal(t, "application/vnd.veraison.nvidia-gpu-evidence+json", GPUEvidenceMediaTypeJSON) } @@ -62,7 +51,7 @@ func Test_GPUEvidence_Valid_Fail_WrongNonceSize(t *testing.T) { evidence := validGPUEvidence() evidence.Devices[0].Nonce = []byte("short") - assert.EqualError(t, evidence.Valid(), `invalid field "[0].nonce": expected 32 bytes, got 5`) + assert.EqualError(t, evidence.Valid(), fmt.Sprintf(`invalid field "[0].nonce": expected %d bytes, got 5`, nvml.CC_GPU_CEC_NONCE_SIZE)) } func Test_GPUEvidence_Valid_Fail_MissingDevices(t *testing.T) { @@ -72,6 +61,19 @@ func Test_GPUEvidence_Valid_Fail_MissingDevices(t *testing.T) { assert.EqualError(t, evidence.Valid(), "missing mandatory GPU evidence device") } +func Test_GPUEvidence_ToJSON_Fail_NilEvidence(t *testing.T) { + var evidence *GPUEvidence + + _, err := evidence.ToJSON() + assert.EqualError(t, err, "JSON encoding failed: nil GPU evidence") +} + +func Test_GPUEvidence_FromJSON_Fail_NilEvidence(t *testing.T) { + var evidence *GPUEvidence + + assert.EqualError(t, evidence.FromJSON([]byte("[]")), "JSON decoding failed: nil GPU evidence") +} + func Test_GPUEvidence_Valid_Fail_InvalidArch(t *testing.T) { evidence := validGPUEvidence() evidence.Devices[0].Arch = "AMPERE" @@ -99,9 +101,16 @@ func Test_GPUEvidence_JSON_WireShape(t *testing.T) { encodedJSON, err := evidence.ToJSON() assert.NoError(t, err) - var wire []gpuDeviceEvidenceWire + var wire []map[string]string assert.NoError(t, json.Unmarshal(encodedJSON, &wire)) - assert.Equal(t, validGPUWireEvidence(), wire) + assert.Equal(t, []map[string]string{ + { + "arch": "HOPPER", + "certificate": gpuCertificate, + "evidence": base64.StdEncoding.EncodeToString(gpuReport), + "nonce": base64.StdEncoding.EncodeToString(gpuNonce), + }, + }, wire) } func Test_GPUEvidence_JSON_SerDes_Pass(t *testing.T) { @@ -115,26 +124,3 @@ func Test_GPUEvidence_JSON_SerDes_Pass(t *testing.T) { assert.True(t, reflect.DeepEqual(evidence, decodedEvidence)) } - -func Test_GPUEvidence_CBOR_WireShape(t *testing.T) { - evidence := validGPUEvidence() - - encodedCBOR, err := cbor.Marshal(evidence) - assert.NoError(t, err) - - var wire []gpuDeviceEvidenceWire - assert.NoError(t, cbor.Unmarshal(encodedCBOR, &wire)) - assert.Equal(t, validGPUWireEvidence(), wire) -} - -func Test_GPUEvidence_CBOR_SerDes_Pass(t *testing.T) { - evidence := validGPUEvidence() - - encodedCBOR, err := cbor.Marshal(evidence) - assert.NoError(t, err) - - decodedEvidence := &GPUEvidence{} - assert.NoError(t, cbor.Unmarshal(encodedCBOR, decodedEvidence)) - - assert.True(t, reflect.DeepEqual(evidence, decodedEvidence)) -} From 55c1eb459c9fd6e007e8b5e08eac15512a76f3ac Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Wed, 22 Jul 2026 12:59:36 -0400 Subject: [PATCH 7/8] Address NVIDIA GPU attester review feedback Signed-off-by: Ian Chin Wang --- attesters/nvgpu/nvgpu.go | 2 +- tokens/gpu-evidence.go | 6 +----- tokens/gpu-evidence_test.go | 7 ------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/attesters/nvgpu/nvgpu.go b/attesters/nvgpu/nvgpu.go index aa3ef42..b9b6c91 100644 --- a/attesters/nvgpu/nvgpu.go +++ b/attesters/nvgpu/nvgpu.go @@ -96,7 +96,7 @@ func (p *Plugin) GetSubAttesterID() *compositor.SubAttesterIDOut { } } -func (p *Plugin) GetSupportedFormats() *compositor.SupportedFormatsOut { +func (p Plugin) GetSupportedFormats() *compositor.SupportedFormatsOut { if p.availabilityErr != nil { return &compositor.SupportedFormatsOut{ Status: &compositor.Status{ diff --git a/tokens/gpu-evidence.go b/tokens/gpu-evidence.go index 0e75da4..cad550b 100644 --- a/tokens/gpu-evidence.go +++ b/tokens/gpu-evidence.go @@ -60,11 +60,7 @@ func (g GPUEvidence) Valid() error { return nil } -func (g *GPUEvidence) ToJSON() ([]byte, error) { - if g == nil { - return nil, errors.New("JSON encoding failed: nil GPU evidence") - } - +func (g GPUEvidence) ToJSON() ([]byte, error) { if err := g.Valid(); err != nil { return nil, fmt.Errorf("JSON encoding failed: %w", err) } diff --git a/tokens/gpu-evidence_test.go b/tokens/gpu-evidence_test.go index 7b3b82e..2b603db 100644 --- a/tokens/gpu-evidence_test.go +++ b/tokens/gpu-evidence_test.go @@ -61,13 +61,6 @@ func Test_GPUEvidence_Valid_Fail_MissingDevices(t *testing.T) { assert.EqualError(t, evidence.Valid(), "missing mandatory GPU evidence device") } -func Test_GPUEvidence_ToJSON_Fail_NilEvidence(t *testing.T) { - var evidence *GPUEvidence - - _, err := evidence.ToJSON() - assert.EqualError(t, err, "JSON encoding failed: nil GPU evidence") -} - func Test_GPUEvidence_FromJSON_Fail_NilEvidence(t *testing.T) { var evidence *GPUEvidence From 9441a2b9d94229375060a8937ab9f3c6d9255b38 Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Wed, 22 Jul 2026 13:07:00 -0400 Subject: [PATCH 8/8] Use value receivers in NVIDIA GPU plugin Signed-off-by: Ian Chin Wang --- attesters/nvgpu/nvgpu.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/attesters/nvgpu/nvgpu.go b/attesters/nvgpu/nvgpu.go index b9b6c91..2235a3e 100644 --- a/attesters/nvgpu/nvgpu.go +++ b/attesters/nvgpu/nvgpu.go @@ -64,7 +64,7 @@ func defaultCollectorFactory() (evidenceCollector, error) { // resources open until evidence collection is requested. The plugin interface // has no initialization hook, so GetSupportedFormats reports this probe's result // and does not advertise a format that the host cannot produce. -func (p *Plugin) initialize() error { +func (p Plugin) initialize() error { collector, err := p.newCollector() if err != nil { return err @@ -82,14 +82,14 @@ func getEvidenceError(e error) *compositor.EvidenceOut { } } -func (p *Plugin) GetOptions() *compositor.OptionsOut { +func (p Plugin) GetOptions() *compositor.OptionsOut { return &compositor.OptionsOut{ Options: []*compositor.Option{}, Status: statusSucceeded, } } -func (p *Plugin) GetSubAttesterID() *compositor.SubAttesterIDOut { +func (p Plugin) GetSubAttesterID() *compositor.SubAttesterIDOut { return &compositor.SubAttesterIDOut{ SubAttesterID: sid, Status: statusSucceeded, @@ -112,7 +112,7 @@ func (p Plugin) GetSupportedFormats() *compositor.SupportedFormatsOut { } } -func (p *Plugin) GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut { +func (p Plugin) GetEvidence(in *compositor.EvidenceIn) *compositor.EvidenceOut { if uint32(len(in.Nonce)) != nonceSize { errMsg := fmt.Errorf( "nonce size of the NVIDIA GPU attester should be %d, got %d",