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:
@@ -4,24 +4,39 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type policyEvidenceVerifier struct {
|
||||
policyPath string
|
||||
policyPath string
|
||||
loadManifest func(string) (*corim.UnsignedCorim, error)
|
||||
rootVerifier func(cocosattestation.PlatformType) (cocosattestation.Verifier, error)
|
||||
newGPUVerifier func() (cocosattestation.Verifier, error)
|
||||
}
|
||||
|
||||
func NewEvidenceVerifier(policyPath string) eaattestation.EvidenceVerifier {
|
||||
return &policyEvidenceVerifier{policyPath: policyPath}
|
||||
return &policyEvidenceVerifier{
|
||||
policyPath: policyPath,
|
||||
loadManifest: loadCoRIM,
|
||||
rootVerifier: platformVerifier,
|
||||
newGPUVerifier: defaultGPUVerifier,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *policyEvidenceVerifier) VerifyEvidence(evidence []byte) error {
|
||||
@@ -32,15 +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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user