COCOS-591: Add support for GPU CC attestation (#592)
CI / lint (push) Has been cancelled
CI / test (agent) (push) Has been cancelled
CI / test (cli) (push) Has been cancelled
CI / test (cmd) (push) Has been cancelled
CI / test (internal) (push) Has been cancelled
CI / test (manager, true) (push) Has been cancelled
CI / test (pkg) (push) Has been cancelled
CI / upload-coverage (push) Has been cancelled

* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* NOISSUE - Enforce binding label check (#589)

* NOISSUE - Implement extensible resource downloader framework with support for S3, GCS, and OCI sources (#590)

* feat: implement extensible resource downloader framework with support for S3, GCS, and OCI sources

Signed-off-by: SammyOina <sammyoina@gmail.com>

* refactor: improve resource URL parsing and add support for bare OCI image references

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* fix: add empty string check and slash requirement for OCI image inference, and update python unit tests with event mock expectations

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* refactor: introduce OCIClient interface, add test coverage for decryption, and improve resource download error handling

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* chore: remove trailing whitespace in OCI downloader and HTTP tests

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: SammyOina <sammyoina@gmail.com>
Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Refactored baed on comments

* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* Refactored baed on comments

* fixed lint error

* fixed tests

* Fixed according to comments

* COCOS-584 - Support multiple kbs (#587)

* feat: Implement per-resource KBS configuration, allowing algorithms and datasets to specify individual KBS URLs.

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* refactor: Encapsulate CLI error handling and CVM certificate paths within the CLI struct, and add algorithm type to agent's algorithm structure.

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* style: Remove blank lines and fix indentation in CLI commands.

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* refactor: Update downloadAndDecryptGenericResource to accept KBS URL as a parameter and adjust related tests

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* refactor: group CLI configuration into structured types and simplify skopeo decryption key handling

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* Refactored baed on comments

* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* Refactored baed on comments

* fixed lint error

* fixed tests

* Fixed according to comments

---------

Signed-off-by: SammyOina <sammyoina@gmail.com>
Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Co-authored-by: Danko Miladinovic <72250944+danko-miladinovic@users.noreply.github.com>
Co-authored-by: Sammy Kerata Oina <44265300+SammyOina@users.noreply.github.com>
This commit is contained in:
Jovan Djukic
2026-05-08 16:35:04 +02:00
committed by GitHub
parent 81fe0b11b5
commit 27db9b29eb
17 changed files with 1974 additions and 33 deletions
+2
View File
@@ -19,6 +19,7 @@ target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
!tools/nvidia-attestation-helper/Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
@@ -29,3 +30,4 @@ Cargo.lock
*.enc
*.key
*.pub
.codex
+47 -2
View File
@@ -1,11 +1,24 @@
BUILD_DIR = build
SERVICES = manager agent cli attestation-service log-forwarder computation-runner egress-proxy ingress-proxy
NVIDIA_ATTESTATION_HELPER = nvidia-attestation-helper
NVIDIA_ATTESTATION_HELPER_DIR = tools/$(NVIDIA_ATTESTATION_HELPER)
NVIDIA_ATTESTATION_HELPER_MANIFEST = $(NVIDIA_ATTESTATION_HELPER_DIR)/Cargo.toml
NVIDIA_ATTESTATION_HELPER_BINARY = $(BUILD_DIR)/$(NVIDIA_ATTESTATION_HELPER)
NVIDIA_ATTESTATION_HELPER_LIB_DIR = $(BUILD_DIR)/lib
NVAT_SDK_CPP_DIR ?= $(firstword $(wildcard $(HOME)/.cargo/git/checkouts/attestation-sdk-*/*/nv-attestation-sdk-cpp))
NVAT_SDK_CPP_BUILD_DIR ?= $(NVAT_SDK_CPP_DIR)/build
NVAT_SDK_HEADER ?= $(NVAT_SDK_CPP_BUILD_DIR)/include/nvat.h
NVAT_SDK_SHARED_LIB ?= $(NVAT_SDK_CPP_BUILD_DIR)/libnvat.so.1
NVAT_SYSTEM_HEADER ?= /usr/include/nvat.h
CARGO ?= cargo
CMAKE ?= cmake
CGO_ENABLED ?= 0
GOARCH ?= amd64
VERSION ?= $(shell git describe --abbrev=0 --tags --always)
COMMIT ?= $(shell git rev-parse HEAD)
TIME ?= $(shell date +%F_%T)
EMBED_ENABLED ?= 0
NVAT_USE_SYSTEM_LIB ?=
INSTALL_DIR ?= /usr/local/bin
CONFIG_DIR ?= /etc/cocos
SERVICE_NAME ?= cocos-manager
@@ -23,14 +36,46 @@ define compile_service
-o ${BUILD_DIR}/cocos-$(1) ./cmd/$(1)
endef
.PHONY: all $(SERVICES) install clean
NVIDIA_ATTESTATION_HELPER_CARGO_ENV = $(if $(filter 1,$(NVAT_USE_SYSTEM_LIB)),NVAT_USE_SYSTEM_LIB=1,)
NVIDIA_ATTESTATION_HELPER_RUSTFLAGS = $(strip $(RUSTFLAGS) $(if $(filter 1,$(NVAT_USE_SYSTEM_LIB)),,-C link-arg=-Wl,-rpath,$$ORIGIN/lib))
.PHONY: all $(SERVICES) $(NVIDIA_ATTESTATION_HELPER) nvidia-attestation-helper-prereqs install clean
all: $(SERVICES)
$(SERVICES):
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(SERVICES): | $(BUILD_DIR)
$(call compile_service,$@)
@if [ "$@" = "cli" ] || [ "$@" = "manager" ]; then $(MAKE) build-igvm; fi
nvidia-attestation-helper-prereqs:
ifeq ($(filter 1,$(NVAT_USE_SYSTEM_LIB)),1)
@test -f $(NVAT_SYSTEM_HEADER) || \
( echo "Missing $(NVAT_SYSTEM_HEADER). Install the NVAT development package or run without NVAT_USE_SYSTEM_LIB=1."; exit 1 )
@ldconfig -p | grep -q libnvat.so.1 || \
( echo "libnvat.so.1 not found in the dynamic linker cache. Install the NVAT runtime package or run without NVAT_USE_SYSTEM_LIB=1."; exit 1 )
else
@if [ -z "$(NVAT_SDK_CPP_DIR)" ]; then \
echo "Unable to locate nv-attestation-sdk-cpp under $$HOME/.cargo/git/checkouts."; \
echo "Run 'cargo fetch --manifest-path $(NVIDIA_ATTESTATION_HELPER_MANIFEST)' first, or install NVAT and use 'make NVAT_USE_SYSTEM_LIB=1 $(NVIDIA_ATTESTATION_HELPER)'."; \
exit 1; \
fi
@if [ ! -f "$(NVAT_SDK_HEADER)" ] || [ ! -f "$(NVAT_SDK_SHARED_LIB)" ]; then \
$(CMAKE) -S $(NVAT_SDK_CPP_DIR) -B $(NVAT_SDK_CPP_BUILD_DIR) && \
$(CMAKE) --build $(NVAT_SDK_CPP_BUILD_DIR); \
fi
endif
$(NVIDIA_ATTESTATION_HELPER): nvidia-attestation-helper-prereqs | $(BUILD_DIR)
RUSTFLAGS='$(NVIDIA_ATTESTATION_HELPER_RUSTFLAGS)' $(NVIDIA_ATTESTATION_HELPER_CARGO_ENV) $(CARGO) build --manifest-path $(NVIDIA_ATTESTATION_HELPER_MANIFEST) --release
install -m 755 $(NVIDIA_ATTESTATION_HELPER_DIR)/target/release/$(NVIDIA_ATTESTATION_HELPER) $(NVIDIA_ATTESTATION_HELPER_BINARY)
@if [ "$(filter 1,$(NVAT_USE_SYSTEM_LIB))" != "1" ]; then \
install -d $(NVIDIA_ATTESTATION_HELPER_LIB_DIR); \
install -m 755 $(NVAT_SDK_SHARED_LIB) $(NVIDIA_ATTESTATION_HELPER_LIB_DIR)/libnvat.so.1; \
fi
protoc:
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative agent/agent.proto
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative manager/manager.proto
+76
View File
@@ -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: gpuNonce,
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[:]
}
+83
View File
@@ -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
}
+33 -4
View File
@@ -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"
@@ -45,6 +47,19 @@ type config struct {
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:
// - Remote attestation verification via KBS
// - Encrypted algorithm/dataset retrieval
// - Per-computation secret provisioning
//
// Example future fields:
// KBSEndpoint string `env:"KBS_ENDPOINT" envDefault:""` // Optional KBS URL
// KBSEnabled bool `env:"KBS_ENABLED" envDefault:"false"`
// KBSTimeout int `env:"KBS_TIMEOUT_SECONDS" envDefault:"30"`
}
func main() {
@@ -218,6 +233,16 @@ 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,
@@ -225,6 +250,7 @@ func main() {
signingKey: signingKey,
eatFormat: cfg.EATFormat,
eatIssuer: cfg.EATIssuer,
gpuCollector: gpuCollector,
}
attestationpb.RegisterAttestationServiceServer(grpcServer, svc)
@@ -261,6 +287,7 @@ type service struct {
signingKey *ecdsa.PrivateKey
eatFormat string
eatIssuer string
gpuCollector attestationgpu.Collector
}
func (s *service) FetchAttestation(ctx context.Context, req *attestationpb.AttestationRequest) (*attestationpb.AttestationResponse, error) {
@@ -319,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)
+80 -4
View File
@@ -4,13 +4,20 @@
package atls
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"time"
eaattestation "github.com/ultravioletrs/cocos/pkg/atls/eaattestation"
cocosattestation "github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
"github.com/ultravioletrs/cocos/pkg/attestation/gpu"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"github.com/veraison/corim/corim"
@@ -18,10 +25,18 @@ import (
type policyEvidenceVerifier struct {
policyPath string
loadManifest func(string) (*corim.UnsignedCorim, error)
rootVerifier func(cocosattestation.PlatformType) (cocosattestation.Verifier, error)
newGPUVerifier func() (cocosattestation.Verifier, error)
}
func NewEvidenceVerifier(policyPath string) eaattestation.EvidenceVerifier {
return &policyEvidenceVerifier{policyPath: policyPath}
return &policyEvidenceVerifier{
policyPath: policyPath,
loadManifest: loadCoRIM,
rootVerifier: platformVerifier,
newGPUVerifier: defaultGPUVerifier,
}
}
func (v *policyEvidenceVerifier) VerifyEvidence(evidence []byte) error {
@@ -32,15 +47,25 @@ func (v *policyEvidenceVerifier) VerifyEvidence(evidence []byte) error {
if err != nil {
return fmt.Errorf("atls: failed to decode EAT evidence: %w", err)
}
manifest, err := loadCoRIM(v.policyPath)
manifest, err := v.loadManifest(v.policyPath)
if err != nil {
return err
}
verifier, err := platformVerifier(platformTypeFromClaims(claims.PlatformType))
verifier, err := v.rootVerifier(platformTypeFromClaims(claims.PlatformType))
if err != nil {
return err
}
return verifier.VerifyWithCoRIM(claims.RawReport, manifest)
if err := verifier.VerifyWithCoRIM(claims.RawReport, manifest); err != nil {
return err
}
if claims.GPUExtensions != nil {
if err := v.verifyGPUEvidence(claims, manifest); err != nil {
return err
}
}
return nil
}
func loadCoRIM(path string) (*corim.UnsignedCorim, error) {
@@ -90,3 +115,54 @@ func platformVerifier(platformType cocosattestation.PlatformType) (cocosattestat
return nil, fmt.Errorf("atls: unsupported platform type: %d", platformType)
}
}
func defaultGPUVerifier() (cocosattestation.Verifier, error) {
timeout := 30 * time.Second
if rawTimeout := os.Getenv("ATLS_GPU_VERIFIER_TIMEOUT"); rawTimeout != "" {
parsed, err := time.ParseDuration(rawTimeout)
if err != nil {
return nil, fmt.Errorf("atls: invalid ATLS_GPU_VERIFIER_TIMEOUT: %w", err)
}
timeout = parsed
}
binaryPath := os.Getenv("ATLS_GPU_VERIFIER_PATH")
if binaryPath == "" {
binaryPath = os.Getenv("ATTESTATION_GPU_HELPER_PATH")
}
return gpu.NewVerifier(binaryPath, timeout)
}
func (v *policyEvidenceVerifier) verifyGPUEvidence(claims *eat.EATClaims, manifest *corim.UnsignedCorim) error {
if len(claims.GPUExtensions.EvidenceJSON) == 0 {
return fmt.Errorf("atls: gpu evidence is empty")
}
expectedNonce := sha256.Sum256(append(append([]byte(nil), claims.Nonce...), []byte(":gpu")...))
if !bytes.Equal(claims.GPUExtensions.Nonce, expectedNonce[:]) {
return fmt.Errorf("atls: gpu nonce binding mismatch")
}
// Guard against replay: a stale self-consistent EvidenceJSON blob can be
// paired with a rewritten outer Nonce unless the inner nonce is also checked.
var envelopes []struct {
Nonce string `json:"nonce"`
}
if err := json.Unmarshal(claims.GPUExtensions.EvidenceJSON, &envelopes); err != nil {
return fmt.Errorf("atls: failed to parse GPU evidence JSON: %w", err)
}
if len(envelopes) == 0 || strings.TrimSpace(envelopes[0].Nonce) == "" {
return fmt.Errorf("atls: GPU evidence JSON missing nonce")
}
if envelopes[0].Nonce != hex.EncodeToString(expectedNonce[:]) {
return fmt.Errorf("atls: gpu evidence nonce mismatch")
}
verifier, err := v.newGPUVerifier()
if err != nil {
return err
}
return verifier.VerifyWithCoRIM(claims.GPUExtensions.EvidenceJSON, manifest)
}
+165
View File
@@ -0,0 +1,165 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package atls
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"testing"
"github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
cocosattestation "github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
"github.com/veraison/corim/corim"
)
type stubVerifier struct {
reports [][]byte
err error
}
func (s *stubVerifier) VerifyWithCoRIM(report []byte, _ *corim.UnsignedCorim) error {
s.reports = append(s.reports, append([]byte(nil), report...))
return s.err
}
func TestPolicyEvidenceVerifierVerifyEvidence_RootOnly(t *testing.T) {
root := &stubVerifier{}
gpu := &stubVerifier{}
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: []byte("session-nonce"),
}))
require.NoError(t, err)
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
assert.Empty(t, gpu.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_RootAndGPU(t *testing.T) {
root := &stubVerifier{}
gpu := &stubVerifier{}
sessionNonce := []byte("session-nonce")
gpuNonce := deriveExpectedGPUNonce(sessionNonce)
gpuNonceHex := hex.EncodeToString(gpuNonce)
evidenceJSON := fmt.Appendf(nil, `[{"nonce":"%s","evidence":"abc","certificate":"def"}]`, gpuNonceHex)
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: sessionNonce,
GPUExtensions: &eat.GPUExtensions{
Nonce: gpuNonce,
EvidenceJSON: evidenceJSON,
},
}))
require.NoError(t, err)
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
assert.Equal(t, [][]byte{evidenceJSON}, gpu.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_GPUNonceMismatch(t *testing.T) {
root := &stubVerifier{}
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: []byte("session-nonce"),
GPUExtensions: &eat.GPUExtensions{
Nonce: []byte("wrong"),
EvidenceJSON: []byte(`[{"nonce":"aabbcc"}]`),
},
}))
require.Error(t, err)
assert.ErrorContains(t, err, "gpu nonce binding mismatch")
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_GPUVerifierError(t *testing.T) {
expectedErr := errors.New("gpu verify failed")
root := &stubVerifier{}
gpu := &stubVerifier{err: expectedErr}
sessionNonce := []byte("session-nonce")
derivedNonce := deriveExpectedGPUNonce(sessionNonce)
gpuEvidenceJSON := fmt.Appendf(nil, `[{"nonce":"%s"}]`, hex.EncodeToString(derivedNonce))
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: sessionNonce,
GPUExtensions: &eat.GPUExtensions{
Nonce: derivedNonce,
EvidenceJSON: gpuEvidenceJSON,
},
}))
require.Error(t, err)
assert.ErrorIs(t, err, expectedErr)
}
func encodeClaims(t *testing.T, claims *eat.EATClaims) []byte {
t.Helper()
b, err := cbor.Marshal(claims)
require.NoError(t, err)
return b
}
func deriveExpectedGPUNonce(sessionNonce []byte) []byte {
sum := sha256.Sum256(append(append([]byte(nil), sessionNonce...), []byte(":gpu")...))
return sum[:]
}
+41 -1
View File
@@ -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,15 @@ func NewEATClaims(report []byte, nonce []byte, platformType attestation.Platform
return nil, err
}
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(claims); err != nil {
return nil, err
}
}
return claims, nil
}
+21
View File
@@ -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"])
}
+138
View File
@@ -0,0 +1,138 @@
// 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 {
Mode string `json:"mode,omitempty"`
NonceHex string `json:"nonce_hex"`
EvidenceJSON json.RawMessage `json:"evidence_json,omitempty"`
}
type helperResponse struct {
Vendor string `json:"vendor,omitempty"`
EvidenceFormat string `json:"evidence_format,omitempty"`
EvidenceJSON json.RawMessage `json:"evidence_json,omitempty"`
ClaimsJSON json.RawMessage `json:"claims_json,omitempty"`
DetachedEATJSON json.RawMessage `json:"detached_eat_json,omitempty"`
}
// NewCommandCollector creates a collector that shells out to a helper binary.
// 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{
Mode: "collect",
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
}
+141
View File
@@ -0,0 +1,141 @@
// 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)
}
if req.Mode != "collect" {
fmt.Fprintln(os.Stderr, "unexpected helper mode")
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)
})
}
}
+231
View File
@@ -0,0 +1,231 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gpu
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/veraison/corim/comid"
"github.com/veraison/corim/corim"
)
const (
DefaultVerifierBinary = "nvidia-attestation-helper"
defaultVerifierTimeout = 30 * time.Second
)
var _ attestation.Verifier = (*verifier)(nil)
type verifier struct {
binaryPath string
timeout time.Duration
execCommandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
}
type evidenceEnvelope struct {
Nonce string `json:"nonce"`
}
// gpuDeviceClaims mirrors the per-device object produced by the NVIDIA
// attestation helper's verify mode (e.g. "GPU-0": { ... }).
type gpuDeviceClaims struct {
HWModel string `json:"hwmodel"`
OEMID string `json:"oemid"`
DriverVersion string `json:"x-nvidia-gpu-driver-version"`
VBIOSVersion string `json:"x-nvidia-gpu-vbios-version"`
SecBoot bool `json:"secboot"`
DebugStatus string `json:"dbgstat"`
MeasurementResult string `json:"measres"`
NonceMatch bool `json:"x-nvidia-gpu-attestation-report-nonce-match"`
SigVerified bool `json:"x-nvidia-gpu-attestation-report-signature-verified"`
FWIDMatch bool `json:"x-nvidia-gpu-attestation-report-cert-chain-fwid-match"`
ArchCheck bool `json:"x-nvidia-gpu-arch-check"`
DriverRIMSigVerified bool `json:"x-nvidia-gpu-driver-rim-signature-verified"`
VBIOSRIMSigVerified bool `json:"x-nvidia-gpu-vbios-rim-signature-verified"`
DriverRIMVersionMatch bool `json:"x-nvidia-gpu-driver-rim-version-match"`
VBIOSRIMVersionMatch bool `json:"x-nvidia-gpu-vbios-rim-version-match"`
AttestationWarning *string `json:"x-nvidia-attestation-warning"`
}
func NewVerifier(binaryPath string, timeout time.Duration) (attestation.Verifier, error) {
if strings.TrimSpace(binaryPath) == "" {
binaryPath = DefaultVerifierBinary
}
if timeout <= 0 {
timeout = defaultVerifierTimeout
}
return &verifier{
binaryPath: binaryPath,
timeout: timeout,
execCommandContext: exec.CommandContext,
}, nil
}
func (v *verifier) VerifyWithCoRIM(report []byte, manifest *corim.UnsignedCorim) error {
if len(report) == 0 {
return fmt.Errorf("gpu evidence is empty")
}
nonceHex, err := evidenceNonce(report)
if err != nil {
return err
}
reqBody, err := json.Marshal(helperRequest{
Mode: "verify",
NonceHex: nonceHex,
EvidenceJSON: append(json.RawMessage(nil), report...),
})
if err != nil {
return fmt.Errorf("failed to marshal GPU verifier request: %w", err)
}
runCtx := context.Background()
cancel := func() {}
if v.timeout > 0 {
runCtx, cancel = context.WithTimeout(runCtx, v.timeout)
}
defer cancel()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd := v.execCommandContext(runCtx, v.binaryPath)
cmd.Stdin = bytes.NewReader(reqBody)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg == "" {
errMsg = err.Error()
}
return fmt.Errorf("gpu verifier helper failed: %s", errMsg)
}
var resp helperResponse
if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
return fmt.Errorf("failed to decode GPU verifier response: %w", err)
}
if len(resp.ClaimsJSON) == 0 {
return fmt.Errorf("gpu verifier response did not contain claims_json")
}
var deviceClaims map[string]gpuDeviceClaims
if err := json.Unmarshal(resp.ClaimsJSON, &deviceClaims); err != nil {
return fmt.Errorf("gpu: failed to parse claims JSON: %w", err)
}
if len(deviceClaims) == 0 {
return fmt.Errorf("gpu: verifier response contained no device claims")
}
return appraiseGPUClaims(deviceClaims, manifest)
}
// appraiseGPUClaims checks mandatory security flags on every device, then
// matches each device's identity against CoRIM reference values when a
// manifest is provided.
func appraiseGPUClaims(devices map[string]gpuDeviceClaims, manifest *corim.UnsignedCorim) error {
for id, c := range devices {
if !c.SecBoot {
return fmt.Errorf("gpu: %s: secure boot not enabled", id)
}
if c.DebugStatus != "disabled" {
return fmt.Errorf("gpu: %s: debug not disabled (got %q)", id, c.DebugStatus)
}
if c.MeasurementResult != "success" {
return fmt.Errorf("gpu: %s: measurement result not success (got %q)", id, c.MeasurementResult)
}
if !c.NonceMatch || !c.SigVerified || !c.FWIDMatch || !c.ArchCheck ||
!c.DriverRIMSigVerified || !c.VBIOSRIMSigVerified ||
!c.DriverRIMVersionMatch || !c.VBIOSRIMVersionMatch {
return fmt.Errorf("gpu: %s: one or more attestation verification flags are false", id)
}
if c.AttestationWarning != nil {
return fmt.Errorf("gpu: %s: attestation warning: %s", id, *c.AttestationWarning)
}
}
if manifest == nil {
return nil
}
// Match each device's identity (model|driver|vbios) against CoRIM digests.
// matchesCoRIM returns true when a digest matches OR when the manifest
// contains no digest entries at all (no GPU policy configured).
for id, c := range devices {
identity := c.HWModel + "|" + c.DriverVersion + "|" + c.VBIOSVersion
digest := sha256.Sum256([]byte(identity))
if !matchesCoRIM(digest[:], manifest) {
return fmt.Errorf("gpu: %s: no CoRIM reference value matched (model=%q driver=%q vbios=%q)",
id, c.HWModel, c.DriverVersion, c.VBIOSVersion)
}
}
return nil
}
// matchesCoRIM returns true when digest matches any reference value digest in
// the manifest, or when the manifest contains no digest entries (treating an
// empty manifest as "no GPU policy configured").
func matchesCoRIM(digest []byte, manifest *corim.UnsignedCorim) bool {
hasAnyDigests := false
for _, tag := range manifest.Tags {
if !bytes.HasPrefix(tag, corim.ComidTag) {
continue
}
var c comid.Comid
if err := c.FromCBOR(tag[len(corim.ComidTag):]); err != nil {
continue
}
if c.Triples.ReferenceValues == nil {
continue
}
for _, rv := range *c.Triples.ReferenceValues {
if rv.Measurements.Valid() != nil {
continue
}
for _, m := range rv.Measurements {
if m.Val.Digests == nil {
continue
}
for _, d := range *m.Val.Digests {
hasAnyDigests = true
if bytes.Equal(d.HashValue, digest) {
return true
}
}
}
}
}
return !hasAnyDigests
}
func evidenceNonce(report []byte) (string, error) {
var envelopes []evidenceEnvelope
if err := json.Unmarshal(report, &envelopes); err != nil {
return "", fmt.Errorf("failed to parse GPU evidence JSON: %w", err)
}
if len(envelopes) == 0 {
return "", fmt.Errorf("gpu evidence did not contain any devices")
}
if strings.TrimSpace(envelopes[0].Nonce) == "" {
return "", fmt.Errorf("gpu evidence nonce is missing")
}
return envelopes[0].Nonce, nil
}
// SetExecCommandContext allows tests to inject a mock exec.CommandContext.
func (v *verifier) SetExecCommandContext(cmdFunc func(ctx context.Context, name string, arg ...string) *exec.Cmd) {
v.execCommandContext = cmdFunc
}
+359
View File
@@ -0,0 +1,359 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gpu
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/veraison/corim/corim"
)
// validClaimsJSON is a well-formed helper response matching the real SDK output
// (hopperClaimsv3_decoded.json from the NVAT SDK test data).
const validClaimsJSON = `{
"GPU-0": {
"hwmodel": "GH100 A01 GSP BROM",
"oemid": "5703",
"x-nvidia-gpu-driver-version": "550.90.07",
"x-nvidia-gpu-vbios-version": "96.00.9F.00.01",
"secboot": true,
"dbgstat": "disabled",
"measres": "success",
"x-nvidia-gpu-attestation-report-nonce-match": true,
"x-nvidia-gpu-attestation-report-signature-verified": true,
"x-nvidia-gpu-attestation-report-cert-chain-fwid-match": true,
"x-nvidia-gpu-arch-check": true,
"x-nvidia-gpu-driver-rim-signature-verified": true,
"x-nvidia-gpu-vbios-rim-signature-verified": true,
"x-nvidia-gpu-driver-rim-version-match": true,
"x-nvidia-gpu-vbios-rim-version-match": true,
"x-nvidia-attestation-warning": null
}
}`
func fakeVerifierExecCommandContext(_ context.Context, name string, arg ...string) *exec.Cmd {
args := append([]string{"-test.run=TestGPUVerifierHelperProcess", "--", name}, arg...)
cmd := exec.Command(os.Args[0], args...)
cmd.Env = append(os.Environ(), "GO_WANT_GPU_VERIFIER_PROCESS=1")
return cmd
}
func TestGPUVerifierHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_GPU_VERIFIER_PROCESS") != "1" {
return
}
args := os.Args
for i := range args {
if args[i] == "--" {
args = args[i+1:]
break
}
}
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "missing verifier binary name")
os.Exit(2)
}
switch args[0] {
case "verifier-error":
fmt.Fprintln(os.Stderr, "simulated verifier failure")
os.Exit(1)
case "verifier-invalid-json":
fmt.Fprintln(os.Stdout, "{not-json")
os.Exit(0)
case "verifier-empty-claims":
fmt.Fprintln(os.Stdout, `{"detached_eat_json":{"overall_result":true}}`)
os.Exit(0)
case "verifier-invalid-claims-format":
fmt.Fprintln(os.Stdout, `{"claims_json":[1,2,3]}`)
os.Exit(0)
case "verifier-empty-device-claims":
fmt.Fprintln(os.Stdout, `{"claims_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)
}
if req.Mode != "verify" {
fmt.Fprintln(os.Stderr, "unexpected verifier mode")
os.Exit(1)
}
if req.NonceHex == "" {
fmt.Fprintln(os.Stderr, "nonce not propagated to verifier helper")
os.Exit(1)
}
if !json.Valid(req.EvidenceJSON) {
fmt.Fprintln(os.Stderr, "invalid evidence_json payload")
os.Exit(1)
}
if !containsNonce(req.EvidenceJSON, req.NonceHex) {
fmt.Fprintln(os.Stderr, "nonce not propagated to verifier")
os.Exit(1)
}
resp := helperResponse{
ClaimsJSON: json.RawMessage(validClaimsJSON),
DetachedEATJSON: json.RawMessage(`{"overall_result":true}`),
}
if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}
}
func TestEvidenceNonce(t *testing.T) {
nonce, err := evidenceNonce([]byte(`[{"nonce":"aabbcc"}]`))
assert.NoError(t, err)
assert.Equal(t, "aabbcc", nonce)
_, err = evidenceNonce([]byte(`[]`))
assert.ErrorContains(t, err, "did not contain any devices")
_, err = evidenceNonce([]byte(`[{}]`))
assert.ErrorContains(t, err, "nonce is missing")
}
func containsNonce(report json.RawMessage, nonce string) bool {
var envelopes []evidenceEnvelope
if err := json.Unmarshal(report, &envelopes); err != nil {
return false
}
if len(envelopes) == 0 {
return false
}
return envelopes[0].Nonce == nonce
}
func TestVerifierVerifyWithCoRIM(t *testing.T) {
v, err := NewVerifier("verifier-success", 0)
require.NoError(t, err)
cmdVerifier, ok := v.(*verifier)
require.True(t, ok)
cmdVerifier.SetExecCommandContext(fakeVerifierExecCommandContext)
report := []byte(`[{"nonce":"aabbcc","evidence":"abc","certificate":"def"}]`)
// nil manifest: CoRIM phase skipped, only mandatory flags checked.
err = v.VerifyWithCoRIM(report, nil)
assert.NoError(t, err)
// Empty manifest: no digest entries → matchesCoRIM returns true → pass.
err = v.VerifyWithCoRIM(report, &corim.UnsignedCorim{})
assert.NoError(t, err)
}
func TestVerifierVerifyWithCoRIMErrors(t *testing.T) {
tests := []struct {
name string
binary string
report []byte
wantError string
}{
{
name: "empty report",
report: nil,
wantError: "gpu evidence is empty",
},
{
name: "invalid json",
report: []byte(`{`),
wantError: "failed to parse GPU evidence JSON",
},
{
name: "helper failure",
binary: "verifier-error",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "gpu verifier helper failed",
},
{
name: "invalid verifier response",
binary: "verifier-invalid-json",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "failed to decode GPU verifier response",
},
{
name: "missing claims",
binary: "verifier-empty-claims",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "gpu verifier response did not contain claims_json",
},
{
name: "invalid claims format",
binary: "verifier-invalid-claims-format",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "gpu: failed to parse claims JSON",
},
{
name: "empty device claims",
binary: "verifier-empty-device-claims",
report: []byte(`[{"nonce":"aabbcc"}]`),
wantError: "gpu: verifier response contained no device claims",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.name == "empty report" || tt.name == "invalid json" {
v, err := NewVerifier("verifier-success", 0)
require.NoError(t, err)
err = v.VerifyWithCoRIM(tt.report, nil)
assert.ErrorContains(t, err, tt.wantError)
return
}
v, err := NewVerifier(tt.binary, 0)
require.NoError(t, err)
cmdVerifier, ok := v.(*verifier)
require.True(t, ok)
cmdVerifier.SetExecCommandContext(fakeVerifierExecCommandContext)
err = v.VerifyWithCoRIM(tt.report, nil)
assert.ErrorContains(t, err, tt.wantError)
})
}
}
func TestAppraiseGPUClaims(t *testing.T) {
warning := "some warning"
validDevice := gpuDeviceClaims{
HWModel: "GH100 A01 GSP BROM",
OEMID: "5703",
DriverVersion: "550.90.07",
VBIOSVersion: "96.00.9F.00.01",
SecBoot: true,
DebugStatus: "disabled",
MeasurementResult: "success",
NonceMatch: true,
SigVerified: true,
FWIDMatch: true,
ArchCheck: true,
DriverRIMSigVerified: true,
VBIOSRIMSigVerified: true,
DriverRIMVersionMatch: true,
VBIOSRIMVersionMatch: true,
AttestationWarning: nil,
}
tests := []struct {
name string
modify func(gpuDeviceClaims) gpuDeviceClaims
wantError string
}{
{
name: "all valid",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { return c },
},
{
name: "secure boot disabled",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.SecBoot = false; return c },
wantError: "secure boot not enabled",
},
{
name: "debug not disabled",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.DebugStatus = "enabled"; return c },
wantError: "debug not disabled",
},
{
name: "measurement result failed",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.MeasurementResult = "failed"; return c },
wantError: "measurement result not success",
},
{
name: "nonce mismatch",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.NonceMatch = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "signature not verified",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.SigVerified = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "fwid mismatch",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.FWIDMatch = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "arch check failed",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.ArchCheck = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "driver RIM sig not verified",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.DriverRIMSigVerified = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "vbios RIM sig not verified",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.VBIOSRIMSigVerified = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "driver RIM version mismatch",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.DriverRIMVersionMatch = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "vbios RIM version mismatch",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.VBIOSRIMVersionMatch = false; return c },
wantError: "one or more attestation verification flags are false",
},
{
name: "attestation warning present",
modify: func(c gpuDeviceClaims) gpuDeviceClaims { c.AttestationWarning = &warning; return c },
wantError: "attestation warning",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devices := map[string]gpuDeviceClaims{
"GPU-0": tt.modify(validDevice),
}
err := appraiseGPUClaims(devices, nil)
if tt.wantError == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.wantError)
}
})
}
}
func TestMatchesCoRIM(t *testing.T) {
digest := []byte("some-32-byte-digest-padding-here")
t.Run("nil tags returns true", func(t *testing.T) {
assert.True(t, matchesCoRIM(digest, &corim.UnsignedCorim{}))
})
t.Run("non-ComidTag prefix is skipped", func(t *testing.T) {
m := &corim.UnsignedCorim{
Tags: []corim.Tag{[]byte{0x01, 0x02, 0x03}},
}
assert.True(t, matchesCoRIM(digest, m))
})
t.Run("unparseable ComidTag payload is skipped", func(t *testing.T) {
bad := append(append([]byte{}, corim.ComidTag...), 0xFF, 0xFE)
m := &corim.UnsignedCorim{Tags: []corim.Tag{bad}}
assert.True(t, matchesCoRIM(digest, m))
})
}
+308
View File
@@ -0,0 +1,308 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nv-attestation-sdk"
version = "1.2.0"
source = "git+https://github.com/NVIDIA/attestation-sdk?branch=main#82d29930f5cbc9d689393844132ac0d733b1f499"
dependencies = [
"nv-attestation-sdk-sys",
]
[[package]]
name = "nv-attestation-sdk-sys"
version = "1.2.0"
source = "git+https://github.com/NVIDIA/attestation-sdk?branch=main#82d29930f5cbc9d689393844132ac0d733b1f499"
dependencies = [
"bindgen",
]
[[package]]
name = "nvidia-attestation-helper"
version = "0.1.0"
dependencies = [
"anyhow",
"nv-attestation-sdk",
"serde",
"serde_json",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -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", rev = "82d29930f5cbc9d689393844132ac0d733b1f499" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+92
View File
@@ -0,0 +1,92 @@
# NVIDIA Attestation Helper
This helper wraps NVIDIA's Rust attestation SDK low-level GPU evidence
collection and verification flows and exposes a tiny JSON stdin/stdout
protocol that the Go attestation service and ATLS verifier can call.
## Request
The helper reads a single JSON object from stdin:
```json
{
"mode": "collect",
"nonce_hex": "aabbccdd"
}
```
For verification, send:
```json
{
"mode": "verify",
"nonce_hex": "aabbccdd",
"evidence_json": [{ "...": "..." }]
}
```
## Response
On success it writes:
```json
{
"vendor": "nvidia",
"evidence_format": "nvat-json",
"evidence_json": { "...": "..." }
}
```
`evidence_json` is the JSON emitted by `GpuEvidence::to_json()`.
Verification responses contain the NVIDIA appraisal outputs:
```json
{
"claims_json": [{ "...": "..." }],
"detached_eat_json": { "...": "..." }
}
```
## Build
Prerequisites:
- 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.
ATLS can use the same helper during TLS-handshake verification:
```bash
export ATLS_GPU_VERIFIER_PATH=/path/to/nvidia-attestation-helper
export ATLS_GPU_VERIFIER_TIMEOUT=30s
```
If `ATLS_GPU_VERIFIER_PATH` is unset, the verifier also falls back to
`ATTESTATION_GPU_HELPER_PATH`.
+124
View File
@@ -0,0 +1,124 @@
use anyhow::{bail, Context};
use nv_attestation_sdk::{
EvidencePolicy, GpuEvidenceSource, GpuLocalVerifier, HttpOptions, Nonce, NvatSdk, OcspClient,
RimStore,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::{stdin, stdout, Write};
#[derive(Debug, Deserialize)]
struct Request {
mode: Option<String>,
nonce_hex: String,
#[serde(default)]
evidence_json: Option<Value>,
}
#[derive(Debug, Serialize)]
struct Response {
#[serde(skip_serializing_if = "Option::is_none")]
vendor: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
evidence_format: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
evidence_json: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
claims_json: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
detached_eat_json: Option<Value>,
}
fn main() -> anyhow::Result<()> {
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 resp = match req.mode.as_deref().unwrap_or("collect") {
"collect" => collect_evidence(&nonce)?,
"verify" => verify_evidence(&nonce, req.evidence_json)?,
other => bail!("unsupported helper mode: {other}"),
};
let mut out = stdout().lock();
serde_json::to_writer(&mut out, &resp).context("failed to write helper response")?;
let _ = out.write_all(b"\n");
Ok(())
}
fn collect_evidence(nonce: &Nonce) -> anyhow::Result<Response> {
let evidence_source =
GpuEvidenceSource::from_nvml().context("failed to create NVML evidence source")?;
let evidence = evidence_source
.collect(nonce)
.context("failed to collect evidence")?;
let evidence_json = evidence
.to_json()
.context("failed to serialize GPU evidence to JSON")?;
let evidence_json: Value =
serde_json::from_str(&evidence_json).context("failed to parse serialized evidence JSON")?;
Ok(Response {
vendor: Some("nvidia"),
evidence_format: Some("nvat-json"),
evidence_json: Some(evidence_json),
claims_json: None,
detached_eat_json: None,
})
}
fn verify_evidence(nonce: &Nonce, evidence_json: Option<Value>) -> anyhow::Result<Response> {
let evidence_json = evidence_json.context("verify mode requires evidence_json")?;
let evidence_json = serde_json::to_string(&evidence_json)
.context("failed to serialize evidence_json request field")?;
let evidence_source = GpuEvidenceSource::from_json_string(&evidence_json)
.context("failed to create JSON evidence source")?;
let evidence = evidence_source
.collect(nonce)
.context("failed to load GPU evidence from JSON")?;
if evidence.is_empty() {
bail!("GPU evidence did not contain any devices");
}
let http_opts = HttpOptions::builder()
.max_retry_count(5)
.connection_timeout_ms(10000)
.request_timeout_ms(30000)
.build()
.context("failed to create HTTP options")?;
let rim_store = RimStore::create_remote(None, None, Some(&http_opts))
.context("failed to create RIM store")?;
let ocsp_client = OcspClient::create_default(None, None, Some(&http_opts))
.context("failed to create OCSP client")?;
let verifier = GpuLocalVerifier::new(&rim_store, &ocsp_client)
.context("failed to create GPU local verifier")?;
let policy = EvidencePolicy::builder()
.verify_rim_signature(true)
.verify_rim_cert_chain(true)
.build()
.context("failed to create evidence policy")?;
let result = verifier
.verify(&evidence, &policy)
.context("failed to verify GPU evidence")?;
let claims_json =
serde_json::from_str(&result.claims_json()?).context("failed to parse claims JSON")?;
let detached_eat_json = result
.eat_json()
.ok()
.map(|raw| serde_json::from_str(&raw).context("failed to parse detached EAT JSON"))
.transpose()?;
Ok(Response {
vendor: None,
evidence_format: None,
evidence_json: None,
claims_json: Some(claims_json),
detached_eat_json,
})
}