Skip to content

Commit

Permalink
Integrate signature discovery client into attestation agent.
Browse files Browse the repository at this point in the history
  • Loading branch information
yawangwang committed Aug 16, 2023
2 parents 9e1f35d + 777c2c2 commit e3d6b05
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 21 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ jobs:
if: runner.os == 'Windows'
- name: Build all modules
run: go build -v ./... ./cmd/... ./launcher/...
- name: Run specific tests under root permission
run: |
GO_EXECUTABLE_PATH=$(which go)
sudo $GO_EXECUTABLE_PATH test -v -run "(TestFetchContainerImageSignatures|TestFetchImageSignaturesDockerPublic)" ./launcher
- name: Test all modules
run: go test -v ./... ./cmd/... ./launcher/...

Expand Down
34 changes: 21 additions & 13 deletions launcher/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/google/go-tpm-tools/cel"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/launcher/internal/oci"
"github.com/google/go-tpm-tools/launcher/verifier"
pb "github.com/google/go-tpm-tools/proto/attest"
)
Expand All @@ -23,6 +24,7 @@ var defaultCELHashAlgo = []crypto.Hash{crypto.SHA256, crypto.SHA1}

type tpmKeyFetcher func(rw io.ReadWriter) (*client.Key, error)
type principalIDTokenFetcher func(audience string) ([][]byte, error)
type containerImageSignaturesFetcher func(ctx context.Context) []oci.Signature

// AttestationAgent is an agent that interacts with GCE's Attestation Service
// to Verify an attestation message. It is an interface instead of a concrete
Expand All @@ -33,24 +35,27 @@ type AttestationAgent interface {
}

type agent struct {
tpm io.ReadWriteCloser
akFetcher tpmKeyFetcher
client verifier.Client
principalFetcher principalIDTokenFetcher
cosCel cel.CEL
tpm io.ReadWriteCloser
akFetcher tpmKeyFetcher
client verifier.Client
principalFetcher principalIDTokenFetcher
signaturesFetcher containerImageSignaturesFetcher
cosCel cel.CEL
}

// CreateAttestationAgent returns an agent capable of performing remote
// attestation using the machine's (v)TPM to GCE's Attestation Service.
// - tpm is a handle to the TPM on the instance
// - akFetcher is a func to fetch an attestation key: see go-tpm-tools/client.
// - principalFetcher is a func to fetch GCE principal tokens for a given audience.
func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher tpmKeyFetcher, verifierClient verifier.Client, principalFetcher principalIDTokenFetcher) AttestationAgent {
// - signaturesFetcher is a func to fetch container image signatures associated with the running workload.
func CreateAttestationAgent(tpm io.ReadWriteCloser, akFetcher tpmKeyFetcher, verifierClient verifier.Client, principalFetcher principalIDTokenFetcher, signaturesFetcher containerImageSignaturesFetcher) AttestationAgent {
return &agent{
tpm: tpm,
client: verifierClient,
akFetcher: akFetcher,
principalFetcher: principalFetcher,
tpm: tpm,
client: verifierClient,
akFetcher: akFetcher,
principalFetcher: principalFetcher,
signaturesFetcher: signaturesFetcher,
}
}

Expand Down Expand Up @@ -79,10 +84,13 @@ func (a *agent) Attest(ctx context.Context) ([]byte, error) {
return nil, err
}

signatures := a.signaturesFetcher(ctx)

resp, err := a.client.VerifyAttestation(ctx, verifier.VerifyAttestationRequest{
Challenge: challenge,
GcpCredentials: principalTokens,
Attestation: attestation,
Challenge: challenge,
GcpCredentials: principalTokens,
Attestation: attestation,
ContainerImageSignatures: signatures,
})
if err != nil {
return nil, err
Expand Down
7 changes: 6 additions & 1 deletion launcher/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/internal/test"
"github.com/google/go-tpm-tools/launcher/internal/oci"
"github.com/google/go-tpm-tools/launcher/verifier/fake"
)

Expand All @@ -22,7 +23,7 @@ func TestAttest(t *testing.T) {
t.Errorf("Failed to generate signing key %v", err)
}
verifierClient := fake.NewClient(fakeSigner)
agent := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, placeholderFetcher)
agent := CreateAttestationAgent(tpm, client.AttestationKeyECC, verifierClient, placeholderFetcher, signaturesPlaceholderFetcher)

tokenBytes, err := agent.Attest(context.Background())
if err != nil {
Expand Down Expand Up @@ -58,3 +59,7 @@ func TestAttest(t *testing.T) {
func placeholderFetcher(_ string) ([][]byte, error) {
return [][]byte{}, nil
}

func signaturesPlaceholderFetcher(_ context.Context) []oci.Signature {
return []oci.Signature{}
}
56 changes: 55 additions & 1 deletion launcher/container_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"path"
"strconv"
"strings"
"sync"
"time"

"cloud.google.com/go/compute/metadata"
Expand All @@ -29,6 +30,8 @@ import (
"github.com/google/go-tpm-tools/cel"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/launcher/agent"
internal "github.com/google/go-tpm-tools/launcher/internal/oci"
"github.com/google/go-tpm-tools/launcher/internal/signaturediscovery"
"github.com/google/go-tpm-tools/launcher/spec"
"github.com/google/go-tpm-tools/launcher/verifier"
"github.com/google/go-tpm-tools/launcher/verifier/rest"
Expand Down Expand Up @@ -79,6 +82,33 @@ const (
defaultRefreshJitter = 0.1
)

func fetchContainerImageSignatures(ctx context.Context, sdClient *signaturediscovery.Client, targetRepos []string, logger *log.Logger) []internal.Signature {
signatures := make([][]internal.Signature, len(targetRepos))

var wg sync.WaitGroup
for i, repo := range targetRepos {
wg.Add(1)
go func(targetRepo string, index int) {
defer wg.Done()
sigs, err := sdClient.FetchImageSignatures(ctx, targetRepo)
if err != nil {
logger.Printf("Failed to fetch signatures from the target repo [%s]: %v", targetRepo, err)
} else {
signatures[index] = sigs
}
}(repo, i)
}
wg.Wait()

var foundSigs []internal.Signature
for _, sigs := range signatures {
if len(sigs) != 0 {
foundSigs = append(foundSigs, sigs...)
}
}
return foundSigs
}

func fetchImpersonatedToken(ctx context.Context, serviceAccount string, audience string, opts ...option.ClientOption) ([]byte, error) {
config := impersonate.IDTokenConfig{
Audience: audience,
Expand Down Expand Up @@ -236,14 +266,38 @@ func NewRunner(ctx context.Context, cdClient *containerd.Client, token oauth2.To
return nil, fmt.Errorf("failed to create REST verifier client: %v", err)
}

// Fetch container image signatures by using signaturediscovery client.
signaturesFetcher := func(ctx context.Context) []internal.Signature {
authToken, err := RetrieveAuthToken(mdsClient)
if err != nil {
logger.Printf("failed to retrieve auth token: %v, using empty auth for image pulling\n", err)
}
updatedSpec, err := spec.GetLaunchSpec(mdsClient)
if err != nil {
logger.Println(err)
// If can not get launchSpec, default to the old one.
updatedSpec = launchSpec
}
sdClient := getSignatureDiscoveryClient(cdClient, authToken, image.Target())
return fetchContainerImageSignatures(ctx, sdClient, updatedSpec.SignedImageRepos, logger)
}

return &ContainerRunner{
container,
launchSpec,
agent.CreateAttestationAgent(tpm, client.GceAttestationKeyECC, verifierClient, principalFetcher),
agent.CreateAttestationAgent(tpm, client.GceAttestationKeyECC, verifierClient, principalFetcher, signaturesFetcher),
logger,
}, nil
}

func getSignatureDiscoveryClient(cdClient *containerd.Client, token oauth2.Token, imageDesc v1.Descriptor) *signaturediscovery.Client {
var remoteOpt containerd.RemoteOpt
if token.Valid() {
remoteOpt = containerd.WithResolver(Resolver(token.AccessToken))
}
return signaturediscovery.New(cdClient, imageDesc, remoteOpt)
}

// getRESTClient returns a REST verifier.Client that points to the given address.
// It defaults to the Attestation Verifier instance at
// https://confidentialcomputing.googleapis.com.
Expand Down
68 changes: 68 additions & 0 deletions launcher/container_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ import (
"github.com/containerd/containerd/defaults"
"github.com/containerd/containerd/namespaces"
"github.com/golang-jwt/jwt/v4"
"github.com/google/go-cmp/cmp"
"github.com/google/go-tpm-tools/cel"
"github.com/google/go-tpm-tools/launcher/internal/signaturediscovery"
"github.com/google/go-tpm-tools/launcher/spec"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/oauth2"
"google.golang.org/api/option"
)
Expand Down Expand Up @@ -413,6 +416,71 @@ type idTokenResp struct {
Token string `json:"token"`
}

func TestFetchContainerImageSignatures(t *testing.T) {
ctx := namespaces.WithNamespace(context.Background(), "test")

testCases := []struct {
name string
targetRepos []string
wantLen int
wantBase64Sigs []string
}{
{
name: "fetchContainerImageSignatures success",
targetRepos: []string{"us-docker.pkg.dev/vegas-codelab-5/cosign-test/base", "us-docker.pkg.dev/vegas-codelab-5/cosign-test/base"},
wantLen: 2,
wantBase64Sigs: []string{
// Check signatures from the OCI image manifest at https://pantheon.corp.google.com/artifacts/docker/vegas-codelab-5/us/cosign-test/base/sha256:1febaa6ac3a5c095435d5276755fb8efcb7f029fefe85cd9bf3ec7de91685b9f;tab=manifest?project=vegas-codelab-5.
"MEUCIQDgoiwMiVl1SAI1iePhH6Oeqztms3IwNtN+w0P92HTqQgIgKjJNcHEy0Ep4g4MH1Vd0gAHvbwH9ahD+jlnMP/rXSGE=",
"MEUCIQDgoiwMiVl1SAI1iePhH6Oeqztms3IwNtN+w0P92HTqQgIgKjJNcHEy0Ep4g4MH1Vd0gAHvbwH9ahD+jlnMP/rXSGE=",
},
},
{
name: "fetchContainerImageSignatures success with nil target repos",
targetRepos: nil,
wantLen: 0,
},
{
name: "fetchContainerImageSignatures success with empty target repos",
targetRepos: []string{},
wantLen: 0,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
signedImageDesc := v1.Descriptor{Digest: "sha256:905a0f3b3d6d0fb37bfa448b9e78f833b73f0b19fc97fed821a09cf49e255df1"}
sdClient := createTestSignatureDiscoveryClient(t, signedImageDesc)
gotSigs := fetchContainerImageSignatures(ctx, sdClient, tc.targetRepos, log.Default())
if len(gotSigs) != tc.wantLen {
t.Errorf("fetchContainerImageSignatures did not return expected signatures for test case %s, got signatures length %d, but want %d", tc.name, len(gotSigs), tc.wantLen)
}
var gotBase64Sigs []string
for _, gotSig := range gotSigs {
base64Sig, err := gotSig.Base64Encoded()
if err != nil {
t.Fatalf("fetchContainerImageSignatures did not return expected base64 signatures for test case %s: %v", tc.name, err)
}
gotBase64Sigs = append(gotBase64Sigs, base64Sig)
}
if !cmp.Equal(gotBase64Sigs, tc.wantBase64Sigs) {
t.Errorf("fetchContainerImageSignatures did not return expected signatures for test case %s, got signatures %v, but want %v", tc.name, gotBase64Sigs, tc.wantBase64Sigs)
}
})
}
}

func createTestSignatureDiscoveryClient(t *testing.T, originalImageDesc v1.Descriptor) *signaturediscovery.Client {
t.Helper()

containerdClient, err := containerd.New(defaults.DefaultAddress)
if err != nil {
t.Skipf("test needs containerd daemon: %v", err)
}
t.Cleanup(func() { containerdClient.Close() })
return signaturediscovery.New(containerdClient, originalImageDesc)
}

func TestFetchImpersonatedToken(t *testing.T) {
expectedEmail := "test2@google.com"

Expand Down
10 changes: 6 additions & 4 deletions launcher/verifier/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package verifier
import (
"context"

"github.com/google/go-tpm-tools/launcher/internal/oci"
attestpb "github.com/google/go-tpm-tools/proto/attest"
)

Expand All @@ -25,11 +26,12 @@ type Challenge struct {

// VerifyAttestationRequest is passed in on VerifyAttestation. It contains the
// Challenge from CreateChallenge, optional GcpCredentials linked to the
// attestation, and the Attestation generated from the TPM.
// attestation, the Attestation generated from the TPM, and optional container image signatures associated with the workload.
type VerifyAttestationRequest struct {
Challenge *Challenge
GcpCredentials [][]byte
Attestation *attestpb.Attestation
Challenge *Challenge
GcpCredentials [][]byte
Attestation *attestpb.Attestation
ContainerImageSignatures []oci.Signature
}

// VerifyAttestationResponse is the response from a successful
Expand Down
9 changes: 7 additions & 2 deletions launcher/verifier/rest/rest_network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/internal/test"
"github.com/google/go-tpm-tools/launcher/agent"
"github.com/google/go-tpm-tools/launcher/internal/oci"
"github.com/google/go-tpm-tools/launcher/verifier"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
Expand Down Expand Up @@ -36,17 +37,21 @@ func testClient(t *testing.T) verifier.Client {
return vClient
}

func testFetcher(_ string) ([][]byte, error) {
func testPrincipalIDTokenFetcher(_ string) ([][]byte, error) {
return [][]byte{}, nil
}

func testContainerImageSignaturesFetcher(_ context.Context) []oci.Signature {
return []oci.Signature{}
}

func TestWithAgent(t *testing.T) {
vClient := testClient(t)

tpm := test.GetTPM(t)
defer client.CheckedClose(t, tpm)

agent := agent.CreateAttestationAgent(tpm, client.AttestationKeyECC, vClient, testFetcher)
agent := agent.CreateAttestationAgent(tpm, client.AttestationKeyECC, vClient, testPrincipalIDTokenFetcher, testContainerImageSignaturesFetcher)
token, err := agent.Attest(context.Background())
if err != nil {
t.Errorf("failed to attest to Attestation Service: %v", err)
Expand Down

0 comments on commit e3d6b05

Please sign in to comment.