Skip to content

Commit 3ced14c

Browse files
authored
integrate go-nvtrust to collect GPU attestation (#676)
1 parent fbcd74a commit 3ced14c

File tree

14 files changed

+876
-243
lines changed

14 files changed

+876
-243
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,15 @@ jobs:
9292
run: go build -v ./keymanager/...
9393
if: runner.os == 'Linux' && matrix.architecture == 'x64'
9494
- name: Build launcher module
95-
run: go build -v ./launcher/...
95+
run: go build -v -ldflags="-extldflags=-Wl,-z,lazy" ./launcher/...
9696
if: runner.os == 'Linux'
9797
- name: Run specific tests under root permission
9898
run: |
9999
GO_EXECUTABLE_PATH=$(which go)
100-
sudo $GO_EXECUTABLE_PATH test -v -run "TestFetchImageSignaturesDockerPublic" ./launcher
100+
sudo $GO_EXECUTABLE_PATH test -v -ldflags="-extldflags=-Wl,-z,lazy" -run "TestFetchImageSignaturesDockerPublic" ./launcher
101101
if: runner.os == 'Linux'
102102
- name: Run all tests in launcher to capture potential data race
103-
run: go test -v -race ./launcher/...
103+
run: go test -v -ldflags="-extldflags=-Wl,-z,lazy" -race ./launcher/...
104104
if: (runner.os == 'Linux') && matrix.architecture == 'x64'
105105
- name: Test all modules except launcher and keymanager
106106
run: go test -v ./... ./cmd/... ./verifier/... -skip='TestCacheConcurrentSetGet|TestHwAttestationPass|TestHardwareAttestationPass'

go.work.sum

Lines changed: 248 additions & 65 deletions
Large diffs are not rendered by default.

launcher/agent/agent.go

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ type attestRoot interface {
7171
Attest(nonce []byte) (any, error)
7272
// ComputeNonce hashes the challenge and extraData using the algorithm preferred by the attestation root.
7373
ComputeNonce(challenge []byte, extraData []byte) []byte
74+
// AddDeviceROTs adds detected device RoTs(root of trust).
75+
AddDeviceROTs([]DeviceROT)
76+
}
77+
78+
// DeviceROT defines an interface for all attached devices to collect attestation.
79+
type DeviceROT interface {
80+
// Attest fetches an attestation from the attached device detected by launcher.
81+
Attest(nonce []byte) (any, error)
7482
}
7583

7684
// AttestAgentOpts contains user generated options when calling the
@@ -98,7 +106,7 @@ type agent struct {
98106
// - principalFetcher is a func to fetch GCE principal tokens for a given audience.
99107
// - signaturesFetcher is a func to fetch container image signatures associated with the running workload.
100108
// - logger will log any partial errors returned by VerifyAttestation.
101-
func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher util.TpmKeyFetcher, verifierClient verifier.Client, principalFetcher principalIDTokenFetcher, sigsFetcher signaturediscovery.Fetcher, launchSpec spec.LaunchSpec, logger logging.Logger) (AttestationAgent, error) {
109+
func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher util.TpmKeyFetcher, verifierClient verifier.Client, principalFetcher principalIDTokenFetcher, sigsFetcher signaturediscovery.Fetcher, launchSpec spec.LaunchSpec, logger logging.Logger, deviceROTs []DeviceROT) (AttestationAgent, error) {
102110
// Fetched the AK and save it, so the agent doesn't need to create a new key everytime
103111
ak, err := akFetcher(tpm)
104112
if err != nil {
@@ -161,6 +169,8 @@ func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher util.TpmKeyFetcher
161169
attestAgent.avRot = tpmAR
162170
}
163171

172+
// Add deviceRoTs to the CPU attestation root.
173+
attestAgent.avRot.AddDeviceROTs(deviceROTs)
164174
return attestAgent, nil
165175
}
166176

@@ -359,11 +369,12 @@ func convertOCIToContainerSignature(ociSig oci.Signature) (*verifier.ContainerSi
359369
}
360370

361371
type tpmAttestRoot struct {
362-
tpmMu sync.Mutex
363-
fetchedAK *client.Key
364-
tpm io.ReadWriteCloser
365-
cosCel gecel.CEL
366-
hashAlgos []crypto.Hash
372+
tpmMu sync.Mutex
373+
fetchedAK *client.Key
374+
tpm io.ReadWriteCloser
375+
cosCel gecel.CEL
376+
hashAlgos []crypto.Hash
377+
deviceROTs []DeviceROT
367378
}
368379

369380
func (t *tpmAttestRoot) GetCEL() gecel.CEL {
@@ -404,10 +415,15 @@ func (t *tpmAttestRoot) ComputeNonce(challenge []byte, extraData []byte) []byte
404415
return finalNonce[:]
405416
}
406417

418+
func (t *tpmAttestRoot) AddDeviceROTs(deviceROTs []DeviceROT) {
419+
t.deviceROTs = append(t.deviceROTs, deviceROTs...)
420+
}
421+
407422
type tdxAttestRoot struct {
408-
tdxMu sync.Mutex
409-
qp *tg.LinuxConfigFsQuoteProvider
410-
cosCel gecel.CEL
423+
tdxMu sync.Mutex
424+
qp *tg.LinuxConfigFsQuoteProvider
425+
cosCel gecel.CEL
426+
deviceROTs []DeviceROT
411427
}
412428

413429
func (t *tdxAttestRoot) GetCEL() gecel.CEL {
@@ -441,10 +457,25 @@ func (t *tdxAttestRoot) Attest(nonce []byte) (any, error) {
441457
return nil, err
442458
}
443459

460+
var nvAtt *models.NvidiaAttestation
461+
for _, deviceRoT := range t.deviceROTs {
462+
att, err := deviceRoT.Attest(nonce)
463+
if err != nil {
464+
return nil, err
465+
}
466+
switch v := att.(type) {
467+
case *models.NvidiaAttestation:
468+
nvAtt = v
469+
default:
470+
return nil, fmt.Errorf("unknown device attestation type: %T", v)
471+
}
472+
}
473+
444474
return &verifier.TDCCELAttestation{
445-
CcelAcpiTable: ccelTable,
446-
CcelData: ccelData,
447-
TdQuote: rawQuote,
475+
CcelAcpiTable: ccelTable,
476+
CcelData: ccelData,
477+
TdQuote: rawQuote,
478+
NvidiaAttestation: nvAtt,
448479
}, nil
449480
}
450481

@@ -459,6 +490,10 @@ func (t *tdxAttestRoot) ComputeNonce(challenge []byte, extraData []byte) []byte
459490
return finalNonce[:]
460491
}
461492

493+
func (t *tdxAttestRoot) AddDeviceROTs(deviceROTs []DeviceROT) {
494+
t.deviceROTs = append(t.deviceROTs, deviceROTs...)
495+
}
496+
462497
// Refresh refreshes the internal state of the attestation agent.
463498
// It will reset the container image signatures for now.
464499
func (a *agent) Refresh(ctx context.Context) error {

launcher/agent/agent_test.go

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
tpmpb "github.com/google/go-tpm-tools/proto/tpm"
3333
"github.com/google/go-tpm-tools/verifier"
3434
"github.com/google/go-tpm-tools/verifier/fake"
35+
"github.com/google/go-tpm-tools/verifier/models"
3536
"github.com/google/go-tpm-tools/verifier/oci"
3637
"github.com/google/go-tpm-tools/verifier/oci/cosign"
3738
"google.golang.org/protobuf/encoding/protojson"
@@ -60,7 +61,7 @@ func TestAttestRacing(t *testing.T) {
6061
}
6162

6263
verifierClient := fake.NewClient(fakeSigner)
63-
agent, err := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, placeholderPrincipalFetcher, signaturediscovery.NewFakeClient(), spec.LaunchSpec{}, logging.SimpleLogger())
64+
agent, err := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, placeholderPrincipalFetcher, signaturediscovery.NewFakeClient(), spec.LaunchSpec{}, logging.SimpleLogger(), nil)
6465
if err != nil {
6566
t.Fatal(err)
6667
}
@@ -112,7 +113,7 @@ func TestAttest(t *testing.T) {
112113

113114
verifierClient := fake.NewClient(fakeSigner)
114115

115-
agent, err := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, tc.principalIDTokenFetcher, tc.containerSignaturesFetcher, tc.launchSpec, logging.SimpleLogger())
116+
agent, err := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, tc.principalIDTokenFetcher, tc.containerSignaturesFetcher, tc.launchSpec, logging.SimpleLogger(), nil)
116117
if err != nil {
117118
t.Fatalf("failed to create an attestation agent %v", err)
118119
}
@@ -651,6 +652,7 @@ func measureFakeEvents(attestAgent AttestationAgent) error {
651652
type fakeTdxAttestRoot struct {
652653
cel gecel.CEL
653654
receivedNonce []byte
655+
deviceRoTS []DeviceROT
654656
}
655657

656658
func (f *fakeTdxAttestRoot) Extend(c gecel.Content) error {
@@ -665,8 +667,23 @@ func (f *fakeTdxAttestRoot) GetCEL() gecel.CEL {
665667

666668
func (f *fakeTdxAttestRoot) Attest(nonce []byte) (any, error) {
667669
f.receivedNonce = nonce
670+
var nvAtt *models.NvidiaAttestation
671+
for _, deviceRoT := range f.deviceRoTS {
672+
att, err := deviceRoT.Attest(nonce)
673+
if err != nil {
674+
return nil, err
675+
}
676+
switch v := att.(type) {
677+
case *models.NvidiaAttestation:
678+
nvAtt = v
679+
default:
680+
return nil, fmt.Errorf("unknown device attestation type: %T", v)
681+
}
682+
}
683+
668684
return &verifier.TDCCELAttestation{
669-
TdQuote: []byte("fake-tdx-quote"),
685+
TdQuote: []byte("fake-tdx-quote"),
686+
NvidiaAttestation: nvAtt,
670687
}, nil
671688
}
672689

@@ -681,6 +698,71 @@ func (f *fakeTdxAttestRoot) ComputeNonce(challenge []byte, extraData []byte) []b
681698
return finalNonce[:]
682699
}
683700

701+
func (f *fakeTdxAttestRoot) AddDeviceROTs(deviceRoTS []DeviceROT) {
702+
f.deviceRoTS = append(f.deviceRoTS, deviceRoTS...)
703+
}
704+
705+
type fakeGPURoT struct{}
706+
707+
func (f *fakeGPURoT) Attest(nonce []byte) (any, error) {
708+
if len(nonce) == 0 {
709+
return nil, fmt.Errorf("fake GPU attestation failed")
710+
}
711+
return &models.NvidiaAttestation{
712+
CCFeature: &models.NvidiaSinglePassthroughAttestation{
713+
GPUInfo: models.GPUInfo{UUID: "fake-gpu-uuid"},
714+
},
715+
}, nil
716+
}
717+
func TestTdxAttestRoot(t *testing.T) {
718+
testCases := []struct {
719+
name string
720+
tdxAttestRoot *fakeTdxAttestRoot
721+
nonce []byte
722+
wantGPU bool
723+
wantPass bool
724+
}{
725+
{
726+
name: "success tdxAttestRoot w/o GPU device",
727+
tdxAttestRoot: &fakeTdxAttestRoot{},
728+
nonce: []byte("test-nonce"),
729+
wantPass: true,
730+
},
731+
{
732+
name: "success tdxAttestRoot w/ GPU device",
733+
tdxAttestRoot: &fakeTdxAttestRoot{
734+
deviceRoTS: []DeviceROT{&fakeGPURoT{}},
735+
},
736+
nonce: []byte("test-nonce"),
737+
wantGPU: true,
738+
wantPass: true,
739+
},
740+
{
741+
name: "failed tdxAttestRoot w/ GPU device",
742+
tdxAttestRoot: &fakeTdxAttestRoot{
743+
deviceRoTS: []DeviceROT{&fakeGPURoT{}},
744+
},
745+
nonce: []byte(""),
746+
wantPass: false,
747+
},
748+
}
749+
750+
for _, tc := range testCases {
751+
t.Run(tc.name, func(t *testing.T) {
752+
attestation, err := tc.tdxAttestRoot.Attest(tc.nonce)
753+
if gotPass := (err == nil); gotPass != tc.wantPass {
754+
t.Errorf("tdxAttestRoot.Attest() did not return expected attestation result, got %v, want %v", gotPass, tc.wantPass)
755+
}
756+
if tc.wantPass && tc.wantGPU {
757+
if att := attestation.(*verifier.TDCCELAttestation); att.NvidiaAttestation == nil {
758+
t.Error("tdxAttestRoot.Attest() did not return expected GPU attestation, want GPU attestation, but got nil")
759+
}
760+
}
761+
})
762+
}
763+
764+
}
765+
684766
func TestAttestationEvidence_TDX_Success(t *testing.T) {
685767
ctx := context.Background()
686768
tpm := test.GetTPM(t)
@@ -749,7 +831,7 @@ func TestAttestationEvidence_TPM_Success(t *testing.T) {
749831
Experiments: experiments.Experiments{
750832
EnableAttestationEvidence: true,
751833
},
752-
}, logging.SimpleLogger())
834+
}, logging.SimpleLogger(), nil)
753835
if err != nil {
754836
t.Fatalf("failed to create agent: %v", err)
755837
}

launcher/cloudbuild.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ steps:
7070
# Build the launcher with CGO enabled
7171
cd launcher/launcher
7272
export CGO_LDFLAGS="-L/workspace/keymanager/target/release"
73-
CGO_ENABLED=1 go build -o ../image/launcher -ldflags="-X 'main.BuildCommit=${SHORT_SHA}'"
73+
go build -o ../image/launcher -ldflags="-extldflags=-Wl,-z,lazy -X 'main.BuildCommit=${SHORT_SHA}'"
7474
7575
- name: 'gcr.io/cloud-builders/gcloud'
7676
id: DownloadExpBinary

launcher/container_runner.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ func NewRunner(ctx context.Context, cdClient *containerd.Client, token oauth2.To
194194
}
195195
specOpts = append(specOpts, cgroupOpts...)
196196

197+
var deviceROTs []agent.DeviceROT
197198
if launchSpec.InstallGpuDriver {
198199
gpuMounts := []specs.Mount{
199200
{
@@ -228,6 +229,7 @@ func NewRunner(ctx context.Context, cdClient *containerd.Client, token oauth2.To
228229
logger.Info(fmt.Sprintf("Detected nvidia device : %s", deviceFile))
229230
specOpts = append(specOpts, oci.WithDevices(deviceFile, deviceFile, "crw-rw-rw-"))
230231
}
232+
deviceROTs = append(deviceROTs, &gpu.NvidiaAttester{})
231233
}
232234

233235
container, err = cdClient.NewContainer(
@@ -294,7 +296,7 @@ func NewRunner(ctx context.Context, cdClient *containerd.Client, token oauth2.To
294296
// Create a new signaturediscovery client to fetch signatures.
295297
sdClient := getSignatureDiscoveryClient(cdClient, mdsClient, image.Target())
296298

297-
attestAgent, err := agent.CreateAttestationAgent(tpm, client.GceAttestationKeyECC, verifierClient, principalFetcherWithImpersonate, sdClient, launchSpec, logger)
299+
attestAgent, err := agent.CreateAttestationAgent(tpm, client.GceAttestationKeyECC, verifierClient, principalFetcherWithImpersonate, sdClient, launchSpec, logger, deviceROTs)
298300
if err != nil {
299301
return nil, err
300302
}

launcher/go.mod

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ go 1.24.0
55
toolchain go1.24.8
66

77
require (
8-
cloud.google.com/go/compute/metadata v0.8.0
9-
cloud.google.com/go/logging v1.13.0
8+
cloud.google.com/go/compute/metadata v0.9.0
9+
cloud.google.com/go/logging v1.13.1
1010
cos.googlesource.com/cos/tools.git v0.0.0-20250414225215-0cf736c0714c
11+
github.com/NVIDIA/go-nvml v0.13.0-1
1112
github.com/cenkalti/backoff/v4 v4.3.0
13+
github.com/confidentsecurity/go-nvtrust v0.2.2
1214
github.com/containerd/containerd v1.7.23
1315
github.com/containerd/containerd/v2 v2.0.1
1416
github.com/coreos/go-systemd/v22 v22.5.0
@@ -22,23 +24,24 @@ require (
2224
github.com/opencontainers/go-digest v1.0.0
2325
github.com/opencontainers/image-spec v1.1.0
2426
github.com/opencontainers/runtime-spec v1.2.0
25-
golang.org/x/oauth2 v0.30.0
26-
google.golang.org/api v0.247.0
27-
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c
28-
google.golang.org/grpc v1.74.2
29-
google.golang.org/protobuf v1.36.9
27+
golang.org/x/oauth2 v0.34.0
28+
google.golang.org/api v0.265.0
29+
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20
30+
google.golang.org/grpc v1.78.0
31+
google.golang.org/protobuf v1.36.11
3032
)
3133

3234
require (
33-
cloud.google.com/go v0.120.0 // indirect
34-
cloud.google.com/go/auth v0.16.4 // indirect
35+
cloud.google.com/go v0.123.0 // indirect
36+
cloud.google.com/go/auth v0.18.1 // indirect
3537
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
36-
cloud.google.com/go/confidentialcomputing v1.9.3-0.20250902151313-51583bd5c9b8 // indirect
37-
cloud.google.com/go/longrunning v0.6.7 // indirect
38+
cloud.google.com/go/confidentialcomputing v1.11.0 // indirect
39+
cloud.google.com/go/longrunning v0.8.0 // indirect
3840
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
3941
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 // indirect
4042
github.com/Microsoft/go-winio v0.6.2 // indirect
4143
github.com/Microsoft/hcsshim v0.12.9 // indirect
44+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4245
github.com/containerd/cgroups/v3 v3.0.3 // indirect
4346
github.com/containerd/containerd/api v1.8.0 // indirect
4447
github.com/containerd/continuity v0.4.4 // indirect
@@ -65,8 +68,8 @@ require (
6568
github.com/google/logger v1.1.1 // indirect
6669
github.com/google/s2a-go v0.1.9 // indirect
6770
github.com/google/uuid v1.6.0 // indirect
68-
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
69-
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
71+
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
72+
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
7073
github.com/klauspost/compress v1.17.11 // indirect
7174
github.com/moby/locker v1.0.1 // indirect
7275
github.com/moby/sys/mountinfo v0.7.2 // indirect
@@ -78,21 +81,21 @@ require (
7881
github.com/pkg/errors v0.9.1 // indirect
7982
github.com/sirupsen/logrus v1.9.3 // indirect
8083
go.opencensus.io v0.24.0 // indirect
81-
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
84+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
8285
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
8386
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
84-
go.opentelemetry.io/otel v1.36.0 // indirect
85-
go.opentelemetry.io/otel/metric v1.36.0 // indirect
86-
go.opentelemetry.io/otel/trace v1.36.0 // indirect
87+
go.opentelemetry.io/otel v1.39.0 // indirect
88+
go.opentelemetry.io/otel/metric v1.39.0 // indirect
89+
go.opentelemetry.io/otel/trace v1.39.0 // indirect
8790
go.uber.org/multierr v1.11.0 // indirect
88-
golang.org/x/crypto v0.45.0 // indirect
89-
golang.org/x/net v0.47.0 // indirect
90-
golang.org/x/sync v0.18.0 // indirect
91-
golang.org/x/sys v0.38.0 // indirect
92-
golang.org/x/text v0.31.0 // indirect
93-
golang.org/x/time v0.12.0 // indirect
94-
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
95-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
91+
golang.org/x/crypto v0.47.0 // indirect
92+
golang.org/x/net v0.49.0 // indirect
93+
golang.org/x/sync v0.19.0 // indirect
94+
golang.org/x/sys v0.40.0 // indirect
95+
golang.org/x/text v0.33.0 // indirect
96+
golang.org/x/time v0.14.0 // indirect
97+
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
98+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
9699
)
97100

98101
replace (

0 commit comments

Comments
 (0)