From a2de1d23ece103617352e13d031796634d66e00b Mon Sep 17 00:00:00 2001 From: Adrian Stobbe Date: Tue, 28 Nov 2023 17:30:11 +0100 Subject: [PATCH] terraform-provider: add attestation data source (#2640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Weiße Co-authored-by: Daniel Weiße --- bazel/ci/terraform.sh.in | 10 +- cli/internal/cmd/BUILD.bazel | 1 - cli/internal/cmd/configfetchmeasurements.go | 76 ++--- .../cmd/configfetchmeasurements_test.go | 127 ++------ cli/internal/cmd/verifier_test.go | 6 - internal/api/attestationconfigapi/fetcher.go | 2 +- internal/attestation/measurements/BUILD.bazel | 8 +- .../measurements/fetchmeasurements.go | 113 +++++++ .../measurements/fetchmeasurements_test.go | 198 ++++++++++++ .../docs/data-sources/attestation.md | 76 +++++ .../constellation_attestation/data-source.tf | 5 + terraform-provider-constellation/go.mod | 3 + terraform-provider-constellation/go.sum | 7 + .../internal/provider/BUILD.bazel | 6 + .../provider/attestation_data_source.go | 306 ++++++++++++++++++ .../provider/attestation_data_source_test.go | 100 ++++++ .../internal/provider/image_data_source.go | 2 - .../internal/provider/provider.go | 2 +- 18 files changed, 871 insertions(+), 177 deletions(-) create mode 100644 internal/attestation/measurements/fetchmeasurements.go create mode 100644 internal/attestation/measurements/fetchmeasurements_test.go create mode 100644 terraform-provider-constellation/docs/data-sources/attestation.md create mode 100644 terraform-provider-constellation/examples/data-sources/constellation_attestation/data-source.tf create mode 100644 terraform-provider-constellation/internal/provider/attestation_data_source.go create mode 100644 terraform-provider-constellation/internal/provider/attestation_data_source_test.go diff --git a/bazel/ci/terraform.sh.in b/bazel/ci/terraform.sh.in index 6d94da984..7f355b815 100644 --- a/bazel/ci/terraform.sh.in +++ b/bazel/ci/terraform.sh.in @@ -71,7 +71,7 @@ check() { for exclude in "${excludeLockDirs[@]}"; do for i in "${!terraformLockModules[@]}"; do if [[ ${terraformLockModules[i]} == "${BUILD_WORKSPACE_DIRECTORY}/${exclude}"* ]]; then - echo " ${terraformLockModules[i]}" + echo "${terraformLockModules[i]}" unset 'terraformLockModules[i]' fi done @@ -119,6 +119,14 @@ check() { ${terraform} -chdir="${module}" fmt -recursive > /dev/null ${terraform} -chdir="${module}" validate > /dev/null rm -rf "${module}/.terraform" + echo "Deleting lock files in the following directories:" # init generates lockfiles which should only be generated in the generate mode. + for dir in "${excludeLockDirs[@]}"; do + if [[ -d ${dir} ]]; then + find "${dir}" -name '*.lock.hcl' -type f -delete + else + echo " Directory ${dir} does not exist, skipping" + fi + done done ;; diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 9b5a8ca95..78491129d 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -170,7 +170,6 @@ go_test( "//internal/license", "//internal/logger", "//internal/semver", - "//internal/sigstore", "//internal/state", "//internal/versions", "//operators/constellation-node-operator/api/v1alpha1", diff --git a/cli/internal/cmd/configfetchmeasurements.go b/cli/internal/cmd/configfetchmeasurements.go index ccaa13a91..705076846 100644 --- a/cli/internal/cmd/configfetchmeasurements.go +++ b/cli/internal/cmd/configfetchmeasurements.go @@ -17,12 +17,13 @@ import ( "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/featureset" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/sigstore" - "github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -73,10 +74,18 @@ func (f *fetchMeasurementsFlags) parse(flags *pflag.FlagSet) error { return nil } +type verifyFetcher interface { + FetchAndVerifyMeasurements(ctx context.Context, + image string, csp cloudprovider.Provider, attestationVariant variant.Variant, + noVerify bool, + ) (measurements.M, error) +} + type configFetchMeasurementsCmd struct { flags fetchMeasurementsFlags canFetchMeasurements bool log debugLog + verifyFetcher verifyFetcher } func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error { @@ -90,19 +99,20 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error { if err != nil { return fmt.Errorf("constructing Rekor client: %w", err) } - cfm := &configFetchMeasurementsCmd{log: log, canFetchMeasurements: featureset.CanFetchMeasurements} + + verifyFetcher := measurements.NewVerifyFetcher(sigstore.NewCosignVerifier, rekor, http.DefaultClient) + cfm := &configFetchMeasurementsCmd{log: log, canFetchMeasurements: featureset.CanFetchMeasurements, verifyFetcher: verifyFetcher} if err := cfm.flags.parse(cmd.Flags()); err != nil { return fmt.Errorf("parsing flags: %w", err) } cfm.log.Debugf("Using flags %+v", cfm.flags) fetcher := attestationconfigapi.NewFetcherWithClient(http.DefaultClient, constants.CDNRepositoryURL) - return cfm.configFetchMeasurements(cmd, sigstore.NewCosignVerifier, rekor, fileHandler, fetcher, http.DefaultClient) + return cfm.configFetchMeasurements(cmd, fileHandler, fetcher) } func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( - cmd *cobra.Command, newCosignVerifier cosignVerifierConstructor, rekor rekorVerifier, - fileHandler file.Handler, fetcher attestationconfigapi.Fetcher, client *http.Client, + cmd *cobra.Command, fileHandler file.Handler, fetcher attestationconfigapi.Fetcher, ) error { if !cfm.canFetchMeasurements { cmd.PrintErrln("Fetching measurements is not supported in the OSS build of the Constellation CLI. Consult the documentation for instructions on where to download the enterprise version.") @@ -132,58 +142,16 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( if err := cfm.flags.updateURLs(conf); err != nil { return err } - - cfm.log.Debugf("Fetching and verifying measurements") - imageVersion, err := versionsapi.NewVersionFromShortPath(conf.Image, versionsapi.VersionKindImage) + fetchedMeasurements, err := cfm.verifyFetcher.FetchAndVerifyMeasurements(ctx, conf.Image, conf.GetProvider(), + conf.GetAttestationConfig().GetVariant(), cfm.flags.insecure) if err != nil { - return err - } - - publicKey, err := keyselect.CosignPublicKeyForVersion(imageVersion) - if err != nil { - return fmt.Errorf("getting public key: %w", err) - } - cosign, err := newCosignVerifier(publicKey) - if err != nil { - return fmt.Errorf("creating cosign verifier: %w", err) - } - - var fetchedMeasurements measurements.M - var hash string - if cfm.flags.insecure { - if err := fetchedMeasurements.FetchNoVerify( - ctx, - client, - cfm.flags.measurementsURL, - imageVersion, - conf.GetProvider(), - conf.GetAttestationConfig().GetVariant(), - ); err != nil { - return fmt.Errorf("fetching measurements without verification: %w", err) - } - - cfm.log.Debugf("Fetched measurements without verification") - } else { - hash, err = fetchedMeasurements.FetchAndVerify( - ctx, - client, - cosign, - cfm.flags.measurementsURL, - cfm.flags.signatureURL, - imageVersion, - conf.GetProvider(), - conf.GetAttestationConfig().GetVariant(), - ) - if err != nil { - return fmt.Errorf("fetching and verifying measurements: %w", err) - } - cfm.log.Debugf("Fetched and verified measurements, hash is %s", hash) - if err := sigstore.VerifyWithRekor(cmd.Context(), publicKey, rekor, hash); err != nil { + var rekorErr *measurements.RekorError + if errors.As(err, &rekorErr) { cmd.PrintErrf("Ignoring Rekor related error: %v\n", err) cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!") + } else { + return fmt.Errorf("fetching and verifying measurements: %w", err) } - - cfm.log.Debugf("Verified measurements with Rekor") } cfm.log.Debugf("Measurements:\n", fetchedMeasurements) @@ -234,5 +202,3 @@ type rekorVerifier interface { SearchByHash(context.Context, string) ([]string, error) VerifyEntry(context.Context, string, string) error } - -type cosignVerifierConstructor func([]byte) (sigstore.Verifier, error) diff --git a/cli/internal/cmd/configfetchmeasurements_test.go b/cli/internal/cmd/configfetchmeasurements_test.go index 7f4aff076..9cebbb7da 100644 --- a/cli/internal/cmd/configfetchmeasurements_test.go +++ b/cli/internal/cmd/configfetchmeasurements_test.go @@ -7,23 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only package cmd import ( - "bytes" "context" - "fmt" - "io" "net/http" "net/url" "testing" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/logger" - "github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -154,114 +151,17 @@ func newTestClient(fn roundTripFunc) *http.Client { } func TestConfigFetchMeasurements(t *testing.T) { - measurements := `{ - "version": "v999.999.999", - "ref": "-", - "stream": "stable", - "list": [ - { - "csp": "GCP", - "attestationVariant":"gcp-sev-es", - "measurements": { - "0": { - "expected": "0000000000000000000000000000000000000000000000000000000000000000", - "warnOnly":false - }, - "1": { - "expected": "1111111111111111111111111111111111111111111111111111111111111111", - "warnOnly":false - }, - "2": { - "expected": "2222222222222222222222222222222222222222222222222222222222222222", - "warnOnly":false - }, - "3": { - "expected": "3333333333333333333333333333333333333333333333333333333333333333", - "warnOnly":false - }, - "4": { - "expected": "4444444444444444444444444444444444444444444444444444444444444444", - "warnOnly":false - }, - "5": { - "expected": "5555555555555555555555555555555555555555555555555555555555555555", - "warnOnly":false - }, - "6": { - "expected": "6666666666666666666666666666666666666666666666666666666666666666", - "warnOnly":false - } - } - } - ] -} -` - signature := "placeholder-signature" - - client := newTestClient(func(req *http.Request) *http.Response { - if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json" { - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewBufferString(measurements)), - Header: make(http.Header), - } - } - if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json.sig" { - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewBufferString(signature)), - Header: make(http.Header), - } - } - - fmt.Println("unexpected request", req.URL.String()) - return &http.Response{ - StatusCode: http.StatusNotFound, - Body: io.NopCloser(bytes.NewBufferString("Not found.")), - Header: make(http.Header), - } - }) - testCases := map[string]struct { - cosign cosignVerifierConstructor - rekor rekorVerifier insecureFlag bool + err error wantErr bool }{ - "success": { - cosign: newStubCosignVerifier, - rekor: singleUUIDVerifier(), + "no error succeeds": {}, + "failing rekor verify should not result in error": { + err: &measurements.RekorError{}, }, - "success without cosign": { - insecureFlag: true, - cosign: func(_ []byte) (sigstore.Verifier, error) { - return &stubCosignVerifier{ - verifyError: assert.AnError, - }, nil - }, - rekor: singleUUIDVerifier(), - }, - "failing search should not result in error": { - cosign: newStubCosignVerifier, - rekor: &stubRekorVerifier{ - SearchByHashUUIDs: []string{}, - SearchByHashError: assert.AnError, - }, - }, - "failing verify should not result in error": { - cosign: newStubCosignVerifier, - rekor: &stubRekorVerifier{ - SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"}, - VerifyEntryError: assert.AnError, - }, - }, - "signature verification failure": { - cosign: func(_ []byte) (sigstore.Verifier, error) { - return &stubCosignVerifier{ - verifyError: assert.AnError, - }, nil - }, - rekor: singleUUIDVerifier(), + "error other than Rekor fails": { + err: assert.AnError, wantErr: true, }, } @@ -279,11 +179,12 @@ func TestConfigFetchMeasurements(t *testing.T) { err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll) require.NoError(err) - cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t)} + fetcher := stubVerifyFetcher{err: tc.err} + cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t), verifyFetcher: fetcher} cfm.flags.insecure = tc.insecureFlag cfm.flags.force = true - err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, stubAttestationFetcher{}, client) + err = cfm.configFetchMeasurements(cmd, fileHandler, stubAttestationFetcher{}) if tc.wantErr { assert.Error(err) return @@ -293,6 +194,14 @@ func TestConfigFetchMeasurements(t *testing.T) { } } +type stubVerifyFetcher struct { + err error +} + +func (f stubVerifyFetcher) FetchAndVerifyMeasurements(_ context.Context, _ string, _ cloudprovider.Provider, _ variant.Variant, _ bool) (measurements.M, error) { + return nil, f.err +} + type stubAttestationFetcher struct{} func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) { diff --git a/cli/internal/cmd/verifier_test.go b/cli/internal/cmd/verifier_test.go index 1011b23ca..b55c0ab15 100644 --- a/cli/internal/cmd/verifier_test.go +++ b/cli/internal/cmd/verifier_test.go @@ -8,8 +8,6 @@ package cmd import ( "context" - - "github.com/edgelesssys/constellation/v2/internal/sigstore" ) // singleUUIDVerifier constructs a RekorVerifier that returns a single UUID and no errors, @@ -43,10 +41,6 @@ type stubCosignVerifier struct { verifyError error } -func newStubCosignVerifier(_ []byte) (sigstore.Verifier, error) { - return &stubCosignVerifier{}, nil -} - func (v *stubCosignVerifier) VerifySignature(_, _ []byte) error { return v.verifyError } diff --git a/internal/api/attestationconfigapi/fetcher.go b/internal/api/attestationconfigapi/fetcher.go index 490edd3f5..a54e3ebc7 100644 --- a/internal/api/attestationconfigapi/fetcher.go +++ b/internal/api/attestationconfigapi/fetcher.go @@ -26,7 +26,7 @@ var ErrNoVersionsFound = errors.New("no versions found") type Fetcher interface { FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionAPI) (SEVSNPVersionAPI, error) FetchSEVSNPVersionList(ctx context.Context, list SEVSNPVersionList) (SEVSNPVersionList, error) - FetchSEVSNPVersionLatest(ctx context.Context, attesation variant.Variant) (SEVSNPVersionAPI, error) + FetchSEVSNPVersionLatest(ctx context.Context, attestation variant.Variant) (SEVSNPVersionAPI, error) } // fetcher fetches AttestationCfg API resources without authentication. diff --git a/internal/attestation/measurements/BUILD.bazel b/internal/attestation/measurements/BUILD.bazel index 07660450d..b3b615e19 100644 --- a/internal/attestation/measurements/BUILD.bazel +++ b/internal/attestation/measurements/BUILD.bazel @@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test") go_library( name = "measurements", srcs = [ + "fetchmeasurements.go", "measurements.go", # keep "measurements_enterprise.go", @@ -16,6 +17,8 @@ go_library( "//internal/api/versionsapi", "//internal/attestation/variant", "//internal/cloud/cloudprovider", + "//internal/sigstore", + "//internal/sigstore/keyselect", "@com_github_google_go_tpm//tpmutil", "@com_github_siderolabs_talos_pkg_machinery//config/encoder", "@in_gopkg_yaml_v3//:yaml_v3", @@ -24,7 +27,10 @@ go_library( go_test( name = "measurements_test", - srcs = ["measurements_test.go"], + srcs = [ + "fetchmeasurements_test.go", + "measurements_test.go", + ], embed = [":measurements"], deps = [ "//internal/api/versionsapi", diff --git a/internal/attestation/measurements/fetchmeasurements.go b/internal/attestation/measurements/fetchmeasurements.go new file mode 100644 index 000000000..7720d1a59 --- /dev/null +++ b/internal/attestation/measurements/fetchmeasurements.go @@ -0,0 +1,113 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measurements + +import ( + "context" + "fmt" + "net/http" + + "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/sigstore" + "github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect" +) + +// RekorError is returned when verifying measurements with Rekor fails. +type RekorError struct { + err error +} + +// Error returns the error message. +func (e *RekorError) Error() string { + return fmt.Sprintf("verifying measurements with Rekor failed: %s", e.err) +} + +// Unwrap returns the wrapped error. +func (e *RekorError) Unwrap() error { + return e.err +} + +// VerifyFetcher is a high-level fetcher that fetches measurements and verifies them. +type VerifyFetcher struct { + client *http.Client + newCosignVerifier cosignVerifierConstructor + rekor rekorVerifier +} + +// NewVerifyFetcher creates a new MeasurementFetcher. +func NewVerifyFetcher(newCosignVerifier func([]byte) (sigstore.Verifier, error), rekor rekorVerifier, client *http.Client) *VerifyFetcher { + return &VerifyFetcher{ + newCosignVerifier: newCosignVerifier, + rekor: rekor, + client: client, + } +} + +// FetchAndVerifyMeasurements fetches and verifies measurements for the given version and attestation variant. +func (m *VerifyFetcher) FetchAndVerifyMeasurements(ctx context.Context, + image string, csp cloudprovider.Provider, attestationVariant variant.Variant, + noVerify bool, +) (M, error) { + version, err := versionsapi.NewVersionFromShortPath(image, versionsapi.VersionKindImage) + if err != nil { + return nil, fmt.Errorf("parsing image version: %w", err) + } + publicKey, err := keyselect.CosignPublicKeyForVersion(version) + if err != nil { + return nil, fmt.Errorf("getting public key: %w", err) + } + + cosign, err := m.newCosignVerifier(publicKey) + if err != nil { + return nil, fmt.Errorf("creating cosign verifier: %w", err) + } + + measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version) + if err != nil { + return nil, err + } + var fetchedMeasurements M + if noVerify { + if err := fetchedMeasurements.FetchNoVerify( + ctx, + m.client, + measurementsURL, + version, + csp, + attestationVariant, + ); err != nil { + return nil, fmt.Errorf("fetching measurements: %w", err) + } + } else { + hash, err := fetchedMeasurements.FetchAndVerify( + ctx, + m.client, + cosign, + measurementsURL, + signatureURL, + version, + csp, + attestationVariant, + ) + if err != nil { + return nil, fmt.Errorf("fetching and verifying measurements: %w", err) + } + if err := sigstore.VerifyWithRekor(ctx, publicKey, m.rekor, hash); err != nil { + return nil, &RekorError{err: err} + } + } + return fetchedMeasurements, nil +} + +type cosignVerifierConstructor func([]byte) (sigstore.Verifier, error) + +type rekorVerifier interface { + SearchByHash(context.Context, string) ([]string, error) + VerifyEntry(context.Context, string, string) error +} diff --git a/internal/attestation/measurements/fetchmeasurements_test.go b/internal/attestation/measurements/fetchmeasurements_test.go new file mode 100644 index 000000000..d79a77a41 --- /dev/null +++ b/internal/attestation/measurements/fetchmeasurements_test.go @@ -0,0 +1,198 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package measurements + +import ( + "bytes" + "context" + "encoding/hex" + "io" + "net/http" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/sigstore" + "github.com/stretchr/testify/assert" +) + +func TestFetchMeasurements(t *testing.T) { + measurements := `{ + "version": "v999.999.999", + "ref": "-", + "stream": "stable", + "list": [ + { + "csp": "GCP", + "attestationVariant":"gcp-sev-es", + "measurements": { + "0": { + "expected": "0000000000000000000000000000000000000000000000000000000000000000", + "warnOnly":false + }, + "1": { + "expected": "1111111111111111111111111111111111111111111111111111111111111111", + "warnOnly":false + }, + "2": { + "expected": "2222222222222222222222222222222222222222222222222222222222222222", + "warnOnly":false + }, + "3": { + "expected": "3333333333333333333333333333333333333333333333333333333333333333", + "warnOnly":false + }, + "4": { + "expected": "4444444444444444444444444444444444444444444444444444444444444444", + "warnOnly":false + }, + "5": { + "expected": "5555555555555555555555555555555555555555555555555555555555555555", + "warnOnly":false + }, + "6": { + "expected": "6666666666666666666666666666666666666666666666666666666666666666", + "warnOnly":true + } + } + } + ] +} +` + signature := "placeholder-signature" + + client := newTestClient(func(req *http.Request) *http.Response { + if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json" { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(measurements)), + Header: make(http.Header), + } + } + if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json.sig" { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(signature)), + Header: make(http.Header), + } + } + + return &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(bytes.NewBufferString("Not found.")), + Header: make(http.Header), + } + }) + + testCases := map[string]struct { + cosign cosignVerifierConstructor + rekor rekorVerifier + noVerify bool + wantErr bool + asRekorErr bool + }{ + "success": { + cosign: newStubCosignVerifier, + rekor: singleUUIDVerifier(), + }, + "success without cosign verify": { + noVerify: true, + cosign: func(_ []byte) (sigstore.Verifier, error) { + return &stubCosignVerifier{ + verifyError: assert.AnError, + }, nil + }, + rekor: singleUUIDVerifier(), + }, + "failing search results is ErrRekor": { + cosign: newStubCosignVerifier, + rekor: &stubRekorVerifier{ + SearchByHashUUIDs: []string{}, + SearchByHashError: assert.AnError, + }, + wantErr: true, + asRekorErr: true, + }, + "failing verify is ErrRekor": { + cosign: newStubCosignVerifier, + rekor: &stubRekorVerifier{ + SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"}, + VerifyEntryError: assert.AnError, + }, + wantErr: true, + asRekorErr: true, + }, + "signature verification failure": { + cosign: func(_ []byte) (sigstore.Verifier, error) { + return &stubCosignVerifier{ + verifyError: assert.AnError, + }, nil + }, + rekor: singleUUIDVerifier(), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + sut := NewVerifyFetcher(tc.cosign, tc.rekor, client) + m, err := sut.FetchAndVerifyMeasurements(context.Background(), "v999.999.999", cloudprovider.GCP, variant.GCPSEVES{}, tc.noVerify) + if tc.wantErr { + assert.Error(err) + if tc.asRekorErr { + var rekErr *RekorError + assert.ErrorAs(err, &rekErr) + } + return + } + assert.NoError(err) + // verify example measurements + assert.Equal("6666666666666666666666666666666666666666666666666666666666666666", hex.EncodeToString(m[6].Expected)) + assert.Equal(WarnOnly, m[6].ValidationOpt) + }) + } +} + +// SubRekorVerifier is a stub for RekorVerifier. +type stubRekorVerifier struct { + SearchByHashUUIDs []string + SearchByHashError error + VerifyEntryError error +} + +// SearchByHash returns the exported fields SearchByHashUUIDs, SearchByHashError. +func (v *stubRekorVerifier) SearchByHash(context.Context, string) ([]string, error) { + return v.SearchByHashUUIDs, v.SearchByHashError +} + +// VerifyEntry returns the exported field VerifyEntryError. +func (v *stubRekorVerifier) VerifyEntry(context.Context, string, string) error { + return v.VerifyEntryError +} + +type stubCosignVerifier struct { + verifyError error +} + +func newStubCosignVerifier(_ []byte) (sigstore.Verifier, error) { + return &stubCosignVerifier{}, nil +} + +func (v *stubCosignVerifier) VerifySignature(_, _ []byte) error { + return v.verifyError +} + +// singleUUIDVerifier constructs a RekorVerifier that returns a single UUID and no errors, +// and should work for most tests on the happy path. +func singleUUIDVerifier() *stubRekorVerifier { + return &stubRekorVerifier{ + SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"}, + SearchByHashError: nil, + VerifyEntryError: nil, + } +} diff --git a/terraform-provider-constellation/docs/data-sources/attestation.md b/terraform-provider-constellation/docs/data-sources/attestation.md new file mode 100644 index 000000000..fb11df2ac --- /dev/null +++ b/terraform-provider-constellation/docs/data-sources/attestation.md @@ -0,0 +1,76 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "constellation_attestation Data Source - constellation" +subcategory: "" +description: |- + The data source to fetch measurements from a configured cloud provider and image. +--- + +# constellation_attestation (Data Source) + +The data source to fetch measurements from a configured cloud provider and image. + +## Example Usage + +```terraform +data "constellation_attestation" "test" { + csp = "aws" + attestation_variant = "aws-sev-snp" + image_version = "v2.13.0" +} +``` + + +## Schema + +### Required + +- `attestation_variant` (String) Attestation variant the image should work with. Can be one of: + * `aws-sev-snp` + * `aws-nitro-tpm` + * `azure-sev-snp` + * `gcp-sev-es` +- `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`) +See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports. +- `image_version` (String) The image version to use + +### Optional + +- `maa_url` (String) For Azure only, the URL of the Microsoft Azure Attestation service + +### Read-Only + +- `attestation` (Attributes) Only relevant for SEV-SNP. (see [below for nested schema](#nestedatt--attestation)) +- `id` (String) The ID of the data source +- `measurements` (Attributes Map) (see [below for nested schema](#nestedatt--measurements)) + + +### Nested Schema for `attestation` + +Read-Only: + +- `amd_root_key` (String) +- `azure_firmware_signer_config` (Attributes) (see [below for nested schema](#nestedatt--attestation--azure_firmware_signer_config)) +- `bootloader_version` (Number) +- `microcode_version` (Number) +- `snp_version` (Number) +- `tee_version` (Number) + + +### Nested Schema for `attestation.azure_firmware_signer_config` + +Read-Only: + +- `accepted_key_digests` (List of String) +- `enforcement_policy` (String) +- `maa_url` (String) + + + + +### Nested Schema for `measurements` + +Read-Only: + +- `expected` (String) +- `warn_only` (Boolean) diff --git a/terraform-provider-constellation/examples/data-sources/constellation_attestation/data-source.tf b/terraform-provider-constellation/examples/data-sources/constellation_attestation/data-source.tf new file mode 100644 index 000000000..4b40c0a69 --- /dev/null +++ b/terraform-provider-constellation/examples/data-sources/constellation_attestation/data-source.tf @@ -0,0 +1,5 @@ +data "constellation_attestation" "test" { + csp = "aws" + attestation_variant = "aws-sev-snp" + image_version = "v2.13.0" +} diff --git a/terraform-provider-constellation/go.mod b/terraform-provider-constellation/go.mod index 64c97bba6..93c3686eb 100644 --- a/terraform-provider-constellation/go.mod +++ b/terraform-provider-constellation/go.mod @@ -21,6 +21,7 @@ require ( github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go v1.44.297 // indirect github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/config v1.18.27 // indirect @@ -65,6 +66,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-containerregistry v0.15.2 // indirect + github.com/google/go-tpm v0.9.0 // indirect github.com/google/uuid v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -112,6 +114,7 @@ require ( github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/schollz/progressbar/v3 v3.13.1 // indirect github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect + github.com/siderolabs/talos/pkg/machinery v1.4.6 // indirect github.com/sigstore/rekor v1.2.2 // indirect github.com/sigstore/sigstore v1.7.1 // indirect github.com/spf13/afero v1.10.0 // indirect diff --git a/terraform-provider-constellation/go.sum b/terraform-provider-constellation/go.sum index 1eb647076..35f496c5b 100644 --- a/terraform-provider-constellation/go.sum +++ b/terraform-provider-constellation/go.sum @@ -60,6 +60,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmms github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.44.297 h1:uL4EV0gQxotQVYegIoBqK079328MOJqgG95daFYSkAM= +github.com/aws/aws-sdk-go v1.44.297/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo= github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= @@ -291,6 +293,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE= github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= +github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= +github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -689,6 +693,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= @@ -765,6 +770,7 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -773,6 +779,7 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= diff --git a/terraform-provider-constellation/internal/provider/BUILD.bazel b/terraform-provider-constellation/internal/provider/BUILD.bazel index c44b25c82..53e8bba32 100644 --- a/terraform-provider-constellation/internal/provider/BUILD.bazel +++ b/terraform-provider-constellation/internal/provider/BUILD.bazel @@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test") go_library( name = "provider", srcs = [ + "attestation_data_source.go", "example_resource.go", "image_data_source.go", "provider.go", @@ -11,9 +12,13 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/provider", visibility = ["//terraform-provider-constellation:__subpackages__"], deps = [ + "//internal/api/attestationconfigapi", + "//internal/attestation/measurements", "//internal/attestation/variant", "//internal/cloud/cloudprovider", + "//internal/config", "//internal/imagefetcher", + "//internal/sigstore", "//terraform-provider-constellation/internal/data", "@com_github_hashicorp_terraform_plugin_framework//datasource", "@com_github_hashicorp_terraform_plugin_framework//datasource/schema", @@ -35,6 +40,7 @@ go_library( go_test( name = "provider_test", srcs = [ + "attestation_data_source_test.go", "image_data_source_test.go", "provider_test.go", ], diff --git a/terraform-provider-constellation/internal/provider/attestation_data_source.go b/terraform-provider-constellation/internal/provider/attestation_data_source.go new file mode 100644 index 000000000..eeced12f9 --- /dev/null +++ b/terraform-provider-constellation/internal/provider/attestation_data_source.go @@ -0,0 +1,306 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package provider + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/sigstore" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &AttestationDataSource{} + +// NewAttestationDataSource creates a new attestation data source. +func NewAttestationDataSource() datasource.DataSource { + return &AttestationDataSource{} +} + +// AttestationDataSource defines the data source implementation. +type AttestationDataSource struct { + client *http.Client + fetcher attestationconfigapi.Fetcher + rekor *sigstore.Rekor +} + +// AttestationDataSourceModel describes the data source data model. +type AttestationDataSourceModel struct { + CSP types.String `tfsdk:"csp"` + AttestationVariant types.String `tfsdk:"attestation_variant"` + ImageVersion types.String `tfsdk:"image_version"` + MaaURL types.String `tfsdk:"maa_url"` + ID types.String `tfsdk:"id"` + Measurements types.Map `tfsdk:"measurements"` + Attestation types.Object `tfsdk:"attestation"` +} + +// Configure configures the data source. +func (d *AttestationDataSource) Configure(_ context.Context, _ datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.client = http.DefaultClient + d.fetcher = attestationconfigapi.NewFetcher() + rekor, err := sigstore.NewRekor() + if err != nil { + resp.Diagnostics.AddError("constructing rekor client", err.Error()) + return + } + d.rekor = rekor +} + +// Metadata returns the metadata for the data source. +func (d *AttestationDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_attestation" +} + +// Schema returns the schema for the data source. +func (d *AttestationDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "The data source to fetch measurements from a configured cloud provider and image.", + + Attributes: map[string]schema.Attribute{ + "csp": schema.StringAttribute{ + Description: "CSP (Cloud Service Provider) to use. (e.g. `azure`)", + MarkdownDescription: "CSP (Cloud Service Provider) to use. (e.g. `azure`)\n" + + "See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("aws", "azure", "gcp"), + }, + }, + "attestation_variant": schema.StringAttribute{ + Description: "Attestation variant the image should work with. (e.g. `azure-sev-snp`)", + MarkdownDescription: "Attestation variant the image should work with. Can be one of:\n" + + " * `aws-sev-snp`\n" + + " * `aws-nitro-tpm`\n" + + " * `azure-sev-snp`\n" + + " * `gcp-sev-es`\n", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("aws-sev-snp", "aws-nitro-tpm", "azure-sev-snp", "gcp-sev-es"), + }, + }, + "image_version": schema.StringAttribute{ + MarkdownDescription: "The image version to use", + Required: true, + }, + "maa_url": schema.StringAttribute{ + MarkdownDescription: "For Azure only, the URL of the Microsoft Azure Attestation service", + Optional: true, + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The ID of the data source", + }, + "measurements": schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "expected": schema.StringAttribute{ + Computed: true, + }, + "warn_only": schema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + "attestation": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: "Only relevant for SEV-SNP.", + Description: "The values provide sensible defaults. See the docs for advanced usage.", // TODO(elchead): AB#3568 + Attributes: map[string]schema.Attribute{ + "bootloader_version": schema.Int64Attribute{ + Computed: true, + }, + "tee_version": schema.Int64Attribute{ + Computed: true, + }, + "snp_version": schema.Int64Attribute{ + Computed: true, + }, + "microcode_version": schema.Int64Attribute{ + Computed: true, + }, + "azure_firmware_signer_config": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "accepted_key_digests": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + }, + "enforcement_policy": schema.StringAttribute{ + Computed: true, + }, + "maa_url": schema.StringAttribute{ + Computed: true, + }, + }, + }, + "amd_root_key": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + } +} + +// Read reads from the data source. +func (d *AttestationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data AttestationDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + csp := cloudprovider.FromString(data.CSP.ValueString()) + if csp == cloudprovider.Unknown { + resp.Diagnostics.AddAttributeError( + path.Root("csp"), + "Invalid CSP", + fmt.Sprintf("Invalid CSP: %s", data.CSP.ValueString()), + ) + return + } + attestationVariant, err := variant.FromString(data.AttestationVariant.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("attestation_variant"), + "Invalid Attestation Variant", + fmt.Sprintf("Invalid attestation variant: %s", data.CSP.ValueString()), + ) + return + } + if attestationVariant.Equal(variant.AzureSEVSNP{}) || attestationVariant.Equal(variant.AWSSEVSNP{}) { + snpVersions, err := d.fetcher.FetchSEVSNPVersionLatest(ctx, attestationVariant) + if err != nil { + resp.Diagnostics.AddError("Fetching SNP Version numbers", err.Error()) + return + } + tfSnpAttestation, err := convertSNPAttestationTfStateCompatible(attestationVariant, snpVersions) + if err != nil { + resp.Diagnostics.AddError("Converting SNP attestation", err.Error()) + } + diags := resp.State.SetAttribute(ctx, path.Root("attestation"), tfSnpAttestation) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } + + verifyFetcher := measurements.NewVerifyFetcher(sigstore.NewCosignVerifier, d.rekor, d.client) + fetchedMeasurements, err := verifyFetcher.FetchAndVerifyMeasurements(ctx, data.ImageVersion.ValueString(), + csp, attestationVariant, false) + if err != nil { + var rekErr *measurements.RekorError + if errors.As(err, &rekErr) { + resp.Diagnostics.AddWarning("Ignoring Rekor related error", err.Error()) + } else { + resp.Diagnostics.AddError("fetching and verifying measurements", err.Error()) + return + } + } + tfMeasurements := convertMeasurementsTfStateCompatible(fetchedMeasurements) + diags := resp.State.SetAttribute(ctx, path.Root("measurements"), tfMeasurements) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Trace(ctx, "read constellation attestation data source") +} + +func convertSNPAttestationTfStateCompatible(attestationVariant variant.Variant, + snpVersions attestationconfigapi.SEVSNPVersionAPI, +) (tfSnpAttestation sevSnpAttestation, err error) { + var cert config.Certificate + switch attestationVariant.(type) { + case variant.AWSSEVSNP: + cert = config.DefaultForAWSSEVSNP().AMDRootKey + case variant.AzureSEVSNP: + cert = config.DefaultForAzureSEVSNP().AMDRootKey + } + certBytes, err := cert.MarshalJSON() + if err != nil { + return tfSnpAttestation, err + } + tfSnpAttestation = sevSnpAttestation{ + BootloaderVersion: snpVersions.Bootloader, + TEEVersion: snpVersions.TEE, + SNPVersion: snpVersions.SNP, + MicrocodeVersion: snpVersions.Microcode, + AMDRootKey: string(certBytes), + } + if attestationVariant.Equal(variant.AzureSEVSNP{}) { + firmwareCfg := config.DefaultForAzureSEVSNP().FirmwareSignerConfig + keyDigestAny, err := firmwareCfg.AcceptedKeyDigests.MarshalYAML() + if err != nil { + return tfSnpAttestation, err + } + keyDigest, ok := keyDigestAny.([]string) + if !ok { + return tfSnpAttestation, errors.New("reading Accepted Key Digests: could not convert to []string") + } + tfSnpAttestation.AzureSNPFirmwareSignerConfig = azureSnpFirmwareSignerConfig{ + AcceptedKeyDigests: keyDigest, + EnforcementPolicy: firmwareCfg.EnforcementPolicy.String(), + MAAURL: firmwareCfg.MAAURL, + } + } + return tfSnpAttestation, nil +} + +func convertMeasurementsTfStateCompatible(m measurements.M) map[string]measurement { + tfMeasurements := map[string]measurement{} + for key, value := range m { + keyStr := strconv.FormatUint(uint64(key), 10) + tfMeasurements[keyStr] = measurement{ + Expected: hex.EncodeToString(value.Expected[:]), + WarnOnly: bool(value.ValidationOpt), + } + } + return tfMeasurements +} + +type measurement struct { + Expected string `tfsdk:"expected"` + WarnOnly bool `tfsdk:"warn_only"` +} + +type sevSnpAttestation struct { + BootloaderVersion uint8 `tfsdk:"bootloader_version"` + TEEVersion uint8 `tfsdk:"tee_version"` + SNPVersion uint8 `tfsdk:"snp_version"` + MicrocodeVersion uint8 `tfsdk:"microcode_version"` + AMDRootKey string `tfsdk:"amd_root_key"` + AzureSNPFirmwareSignerConfig azureSnpFirmwareSignerConfig `tfsdk:"azure_firmware_signer_config"` +} + +type azureSnpFirmwareSignerConfig struct { + AcceptedKeyDigests []string `tfsdk:"accepted_key_digests"` + EnforcementPolicy string `tfsdk:"enforcement_policy"` + MAAURL string `tfsdk:"maa_url"` +} diff --git a/terraform-provider-constellation/internal/provider/attestation_data_source_test.go b/terraform-provider-constellation/internal/provider/attestation_data_source_test.go new file mode 100644 index 000000000..21341c0ed --- /dev/null +++ b/terraform-provider-constellation/internal/provider/attestation_data_source_test.go @@ -0,0 +1,100 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccAttestationSource(t *testing.T) { + // Set the path to the Terraform binary for acceptance testing when running under Bazel. + bazelPreCheck := func() { bazelSetTerraformBinaryPath(t) } + + testCases := map[string]resource.TestCase{ + "aws sev-snp succcess": { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: bazelPreCheck, + Steps: []resource.TestStep{ + { + Config: testingConfig + ` + data "constellation_attestation" "test" { + csp = "aws" + attestation_variant = "aws-sev-snp" + image_version = "v2.13.0" + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.bootloader_version", "3"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.microcode_version", "209"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.snp_version", "20"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.tee_version", "0"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.amd_root_key", "\"-----BEGIN CERTIFICATE-----\\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC\\nBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS\\nBgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg\\nQ2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp\\nY2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy\\nMTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS\\nBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j\\nZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG\\n9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg\\nW41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta\\n1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2\\nSzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0\\n60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05\\ngmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg\\nbKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs\\n+gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi\\nQi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ\\neTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18\\nfHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j\\nWhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI\\nrFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG\\nKWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG\\nSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI\\nAWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel\\nETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw\\nSTjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK\\ndHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq\\nzT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp\\nKGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e\\npmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq\\nHnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh\\n3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn\\nJZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH\\nCViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4\\nAFZEAwoKCQ==\\n-----END CERTIFICATE-----\\n\""), + + resource.TestCheckResourceAttr("data.constellation_attestation.test", "measurements.0.expected", "7b068c0c3ac29afe264134536b9be26f1d4ccd575b88d3c3ceabf36ac99c0278"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "measurements.0.warn_only", "true"), + ), + }, + }, + }, + "azure sev-snp success": { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: bazelPreCheck, + Steps: []resource.TestStep{ + { + Config: testingConfig + ` + data "constellation_attestation" "test" { + csp = "azure" + attestation_variant = "azure-sev-snp" + image_version = "v2.13.0" + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.bootloader_version", "3"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.microcode_version", "115"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.snp_version", "8"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.tee_version", "0"), + + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.azure_firmware_signer_config.accepted_key_digests.0", "0356215882a825279a85b300b0b742931d113bf7e32dde2e50ffde7ec743ca491ecdd7f336dc28a6e0b2bb57af7a44a3"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.azure_firmware_signer_config.enforcement_policy", "MAAFallback"), + + resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.amd_root_key", "\"-----BEGIN CERTIFICATE-----\\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC\\nBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS\\nBgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg\\nQ2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp\\nY2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy\\nMTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS\\nBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j\\nZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG\\n9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg\\nW41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta\\n1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2\\nSzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0\\n60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05\\ngmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg\\nbKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs\\n+gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi\\nQi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ\\neTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18\\nfHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j\\nWhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI\\nrFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG\\nKWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG\\nSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI\\nAWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel\\nETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw\\nSTjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK\\ndHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq\\nzT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp\\nKGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e\\npmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq\\nHnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh\\n3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn\\nJZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH\\nCViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4\\nAFZEAwoKCQ==\\n-----END CERTIFICATE-----\\n\""), + + resource.TestCheckResourceAttr("data.constellation_attestation.test", "measurements.1.expected", "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "measurements.1.warn_only", "true"), + ), + }, + }, + }, + "gcp sev-snp succcess": { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: bazelPreCheck, + Steps: []resource.TestStep{ + { + Config: testingConfig + ` + data "constellation_attestation" "test" { + csp = "gcp" + attestation_variant = "gcp-sev-es" + image_version = "v2.13.0" + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.constellation_attestation.test", "measurements.1.expected", "745f2fb4235e4647aa0ad5ace781cd929eb68c28870e7dd5d1a1535854325e56"), + resource.TestCheckResourceAttr("data.constellation_attestation.test", "measurements.1.warn_only", "true"), + ), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + resource.Test(t, tc) + }) + } +} diff --git a/terraform-provider-constellation/internal/provider/image_data_source.go b/terraform-provider-constellation/internal/provider/image_data_source.go index 0f41055bc..52fb09ac9 100644 --- a/terraform-provider-constellation/internal/provider/image_data_source.go +++ b/terraform-provider-constellation/internal/provider/image_data_source.go @@ -160,8 +160,6 @@ func (d *ImageDataSource) Read(ctx context.Context, req datasource.ReadRequest, // Save data into Terraform state data.Reference = types.StringValue(imageRef) - // Use a placeholder ID for testing, as per https://developer.hashicorp.com/terraform/plugin/framework/acctests#no-id-found-in-attributes - data.ID = types.StringValue("placeholder") resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/terraform-provider-constellation/internal/provider/provider.go b/terraform-provider-constellation/internal/provider/provider.go index 9ba864756..fdf496733 100644 --- a/terraform-provider-constellation/internal/provider/provider.go +++ b/terraform-provider-constellation/internal/provider/provider.go @@ -85,6 +85,6 @@ func (p *ConstellationProvider) Resources(_ context.Context) []func() resource.R // DataSources lists the data sources implemented by the provider. func (p *ConstellationProvider) DataSources(_ context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ - NewImageDataSource, + NewImageDataSource, NewAttestationDataSource, } }