mirror of
https://github.com/ultravioletrs/cocos.git
synced 2026-06-23 04:10:25 +00:00
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
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 commitd81d67e73d. * Revert "Bug" This reverts commit5e566d53c1. * Revert "Possible bug" This reverts commit47d13fe583. * Revert "Possible IGVM script bug" This reverts commit3fb1b79537. * Revert "changed attestion-service.service so it knows where the helper is" This reverts commitf9f11ed183. * Revert "Added command for installing all services" This reverts commit5dcf7a5c0a. * 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 commitd81d67e73d. * Revert "Bug" This reverts commit5e566d53c1. * Revert "Possible bug" This reverts commit47d13fe583. * Revert "Possible IGVM script bug" This reverts commit3fb1b79537. * Revert "changed attestion-service.service so it knows where the helper is" This reverts commitf9f11ed183. * Revert "Added command for installing all services" This reverts commit5dcf7a5c0a. * 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 commitd81d67e73d. * Revert "Bug" This reverts commit5e566d53c1. * Revert "Possible bug" This reverts commit47d13fe583. * Revert "Possible IGVM script bug" This reverts commit3fb1b79537. * Revert "changed attestion-service.service so it knows where the helper is" This reverts commitf9f11ed183. * Revert "Added command for installing all services" This reverts commit5dcf7a5c0a. * 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 commitd81d67e73d. * Revert "Bug" This reverts commit5e566d53c1. * Revert "Possible bug" This reverts commit47d13fe583. * Revert "Possible IGVM script bug" This reverts commit3fb1b79537. * Revert "changed attestion-service.service so it knows where the helper is" This reverts commitf9f11ed183. * Revert "Added command for installing all services" This reverts commit5dcf7a5c0a. * 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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[:]
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
attestationpb "github.com/ultravioletrs/cocos/internal/proto/attestation/v1"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation"
|
||||
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
|
||||
)
|
||||
|
||||
func TestRequestNonce(t *testing.T) {
|
||||
req := &attestationpb.AttestationRequest{
|
||||
ReportData: []byte("report"),
|
||||
Nonce: []byte("nonce"),
|
||||
}
|
||||
|
||||
assert.Equal(t, []byte("nonce"), requestNonce(req))
|
||||
|
||||
req.Nonce = nil
|
||||
assert.Equal(t, []byte("report"), requestNonce(req))
|
||||
}
|
||||
|
||||
func TestDeriveComponentNonce(t *testing.T) {
|
||||
sessionNonce := []byte("session-nonce")
|
||||
|
||||
gpuNonce := deriveComponentNonce(sessionNonce, "gpu")
|
||||
gpuNonceAgain := deriveComponentNonce(sessionNonce, "gpu")
|
||||
teeNonce := deriveComponentNonce(sessionNonce, "tee")
|
||||
|
||||
assert.Len(t, gpuNonce, 32)
|
||||
assert.Equal(t, gpuNonce, gpuNonceAgain)
|
||||
assert.NotEqual(t, gpuNonce, teeNonce)
|
||||
}
|
||||
|
||||
func TestShouldCollectGPU(t *testing.T) {
|
||||
assert.True(t, shouldCollectGPU(attestation.SNP))
|
||||
assert.True(t, shouldCollectGPU(attestation.SNPvTPM))
|
||||
assert.True(t, shouldCollectGPU(attestation.TDX))
|
||||
assert.False(t, shouldCollectGPU(attestation.VTPM))
|
||||
assert.False(t, shouldCollectGPU(attestation.NoCC))
|
||||
}
|
||||
|
||||
func TestNewGPUCollector(t *testing.T) {
|
||||
collector, err := newGPUCollector(config{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, collector)
|
||||
|
||||
collector, err = newGPUCollector(config{
|
||||
GPUHelperPath: "/tmp/helper",
|
||||
GPUHelperTimeout: 0,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, collector)
|
||||
}
|
||||
|
||||
func TestClaimOptions_SkipsOptionalGPUFailure(t *testing.T) {
|
||||
svc := &service{
|
||||
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
gpuCollector: failingCollector{},
|
||||
}
|
||||
|
||||
req := &attestationpb.AttestationRequest{
|
||||
ReportData: []byte("report-data"),
|
||||
Nonce: []byte("nonce-data"),
|
||||
}
|
||||
|
||||
opts, err := svc.claimOptions(context.Background(), req, attestation.TDX)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, opts)
|
||||
}
|
||||
|
||||
type failingCollector struct{}
|
||||
|
||||
func (failingCollector) Collect(context.Context, []byte) (*attestationgpu.Evidence, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
mglog "github.com/absmach/supermq/logger"
|
||||
"github.com/caarlos0/env/v11"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/ccaa"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
|
||||
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
|
||||
logclient "github.com/ultravioletrs/cocos/pkg/clients/grpc/log"
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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[:]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -202,3 +202,24 @@ func TestNewEATClaims_Platforms(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEATClaims_WithGPU(t *testing.T) {
|
||||
gpuEvidence := &GPUExtensions{
|
||||
Vendor: "nvidia",
|
||||
EvidenceFormat: "nvat-json",
|
||||
Nonce: []byte("gpu-nonce"),
|
||||
EvidenceJSON: []byte(`{"evidence":"gpu"}`),
|
||||
}
|
||||
|
||||
claims, err := NewEATClaims(
|
||||
[]byte("dummy report"),
|
||||
[]byte("12345678"),
|
||||
attestation.NoCC,
|
||||
WithGPU(gpuEvidence),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, claims.GPUExtensions)
|
||||
assert.Equal(t, gpuEvidence, claims.GPUExtensions)
|
||||
assert.Contains(t, claims.Submods, "gpu")
|
||||
assert.Equal(t, gpuEvidence, claims.Submods["gpu"])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
@@ -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`.
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user