diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 4717e3c9a..2428cc7bc 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -160,6 +160,7 @@ go_test( "//internal/versions", "//operators/constellation-node-operator/api/v1alpha1", "//verify/verifyproto", + "@com_github_google_go_tpm_tools//proto/tpm", "@com_github_spf13_afero//:afero", "@com_github_spf13_cobra//:cobra", "@com_github_stretchr_testify//assert", diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 541e89846..8d4151d34 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -19,6 +19,7 @@ import ( "net" "net/http" "net/url" + "sort" "strconv" "strings" @@ -381,19 +382,23 @@ func (f *attestationDocFormatterImpl) parseCerts(b *strings.Builder, certTypeNam // parseQuotes parses the base64-encoded quotes and writes their details to the output builder. func (f *attestationDocFormatterImpl) parseQuotes(b *strings.Builder, quotes []*tpmProto.Quote, expectedPCRs measurements.M) error { writeIndentfln(b, 1, "Quote:") - for pcrNum, expectedPCR := range expectedPCRs { + + var pcrNumbers []uint32 + for pcrNum := range expectedPCRs { + pcrNumbers = append(pcrNumbers, pcrNum) + } + sort.Slice(pcrNumbers, func(i, j int) bool { return pcrNumbers[i] < pcrNumbers[j] }) + + for _, pcrNum := range pcrNumbers { + expectedPCR := expectedPCRs[pcrNum] pcrIdx, err := vtpm.GetSHA256QuoteIndex(quotes) if err != nil { return fmt.Errorf("get SHA256 quote index: %w", err) } - if quotes[pcrIdx] == nil { - return fmt.Errorf("quote %d is nil", pcrIdx) - } - - actualPCR := quotes[pcrIdx].Pcrs.Pcrs[pcrNum] - if err != nil { - return fmt.Errorf("decode PCR %d: %w", pcrNum, err) + actualPCR, ok := quotes[pcrIdx].Pcrs.Pcrs[pcrNum] + if !ok { + return fmt.Errorf("PCR %d not found in quote", pcrNum) } writeIndentfln(b, 2, "PCR %d (Strict: %t):", pcrNum, !expectedPCR.ValidationOpt) writeIndentfln(b, 3, "Expected:\t%x", expectedPCR.Expected) diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index 04be43c2f..8af128f82 100644 --- a/cli/internal/cmd/verify_test.go +++ b/cli/internal/cmd/verify_test.go @@ -29,6 +29,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/grpc/testdialer" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/verify/verifyproto" + tpmProto "github.com/google/go-tpm-tools/proto/tpm" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -466,3 +467,93 @@ func TestAddPortIfMissing(t *testing.T) { }) } } + +func TestParseQuotes(t *testing.T) { + testCases := map[string]struct { + quotes []*tpmProto.Quote + expectedPCRs measurements.M + wantOutput string + wantErr bool + }{ + "parse quotes in order": { + quotes: []*tpmProto.Quote{ + { + Pcrs: &tpmProto.PCRs{ + Hash: tpmProto.HashAlgo_SHA256, + Pcrs: map[uint32][]byte{ + 0: {0x00}, + 1: {0x01}, + }, + }, + }, + }, + expectedPCRs: measurements.M{ + 0: measurements.WithAllBytes(0x00, measurements.Enforce, 1), + 1: measurements.WithAllBytes(0x01, measurements.WarnOnly, 1), + }, + wantOutput: "\tQuote:\n\t\tPCR 0 (Strict: true):\n\t\t\tExpected:\t00\n\t\t\tActual:\t\t00\n\t\tPCR 1 (Strict: false):\n\t\t\tExpected:\t01\n\t\t\tActual:\t\t01\n", + }, + "additional quotes are skipped": { + quotes: []*tpmProto.Quote{ + { + Pcrs: &tpmProto.PCRs{ + Hash: tpmProto.HashAlgo_SHA256, + Pcrs: map[uint32][]byte{ + 0: {0x00}, + 1: {0x01}, + 2: {0x02}, + 3: {0x03}, + }, + }, + }, + }, + expectedPCRs: measurements.M{ + 0: measurements.WithAllBytes(0x00, measurements.Enforce, 1), + 1: measurements.WithAllBytes(0x01, measurements.WarnOnly, 1), + }, + wantOutput: "\tQuote:\n\t\tPCR 0 (Strict: true):\n\t\t\tExpected:\t00\n\t\t\tActual:\t\t00\n\t\tPCR 1 (Strict: false):\n\t\t\tExpected:\t01\n\t\t\tActual:\t\t01\n", + }, + "missing quotes error": { + quotes: []*tpmProto.Quote{ + { + Pcrs: &tpmProto.PCRs{ + Hash: tpmProto.HashAlgo_SHA256, + Pcrs: map[uint32][]byte{ + 0: {0x00}, + }, + }, + }, + }, + expectedPCRs: measurements.M{ + 0: measurements.WithAllBytes(0x00, measurements.Enforce, 1), + 1: measurements.WithAllBytes(0x01, measurements.WarnOnly, 1), + }, + wantErr: true, + }, + "no quotes error": { + quotes: []*tpmProto.Quote{}, + expectedPCRs: measurements.M{ + 0: measurements.WithAllBytes(0x00, measurements.Enforce, 1), + 1: measurements.WithAllBytes(0x01, measurements.WarnOnly, 1), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + b := &strings.Builder{} + parser := &attestationDocFormatterImpl{} + + err := parser.parseQuotes(b, tc.quotes, tc.expectedPCRs) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.wantOutput, b.String()) + } + }) + } +} diff --git a/internal/attestation/aws/snp/validator.go b/internal/attestation/aws/snp/validator.go index b9893b0d7..7ecc52885 100644 --- a/internal/attestation/aws/snp/validator.go +++ b/internal/attestation/aws/snp/validator.go @@ -80,12 +80,18 @@ func (v *Validator) tpmEnabled(attestation vtpm.AttestationDocument, _ *attest.M if err != nil { return err } + if len(imageOutput.Images) == 0 { + return fmt.Errorf("aws image %s not found", imageID) + } + if len(imageOutput.Images) > 1 { + return fmt.Errorf("found multiple image references for image ID %s", imageID) + } if imageOutput.Images[0].TpmSupport == "v2.0" { return nil } - return fmt.Errorf("iam image %s does not support TPM v2.0", imageID) + return fmt.Errorf("aws image %s does not support TPM v2.0", imageID) } func getEC2Client(ctx context.Context, region string) (awsMetadataAPI, error) {