terraform-provider: add attestation data source (#2640)

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
Co-authored-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Adrian Stobbe 2023-11-28 17:30:11 +01:00 committed by GitHub
parent 03c5692fdd
commit a2de1d23ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 871 additions and 177 deletions

View File

@ -71,7 +71,7 @@ check() {
for exclude in "${excludeLockDirs[@]}"; do for exclude in "${excludeLockDirs[@]}"; do
for i in "${!terraformLockModules[@]}"; do for i in "${!terraformLockModules[@]}"; do
if [[ ${terraformLockModules[i]} == "${BUILD_WORKSPACE_DIRECTORY}/${exclude}"* ]]; then if [[ ${terraformLockModules[i]} == "${BUILD_WORKSPACE_DIRECTORY}/${exclude}"* ]]; then
echo " ${terraformLockModules[i]}" echo "${terraformLockModules[i]}"
unset 'terraformLockModules[i]' unset 'terraformLockModules[i]'
fi fi
done done
@ -119,6 +119,14 @@ check() {
${terraform} -chdir="${module}" fmt -recursive > /dev/null ${terraform} -chdir="${module}" fmt -recursive > /dev/null
${terraform} -chdir="${module}" validate > /dev/null ${terraform} -chdir="${module}" validate > /dev/null
rm -rf "${module}/.terraform" 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 done
;; ;;

View File

@ -170,7 +170,6 @@ go_test(
"//internal/license", "//internal/license",
"//internal/logger", "//internal/logger",
"//internal/semver", "//internal/semver",
"//internal/sigstore",
"//internal/state", "//internal/state",
"//internal/versions", "//internal/versions",
"//operators/constellation-node-operator/api/v1alpha1", "//operators/constellation-node-operator/api/v1alpha1",

View File

@ -17,12 +17,13 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "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/config"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/featureset" "github.com/edgelesssys/constellation/v2/internal/featureset"
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -73,10 +74,18 @@ func (f *fetchMeasurementsFlags) parse(flags *pflag.FlagSet) error {
return nil 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 { type configFetchMeasurementsCmd struct {
flags fetchMeasurementsFlags flags fetchMeasurementsFlags
canFetchMeasurements bool canFetchMeasurements bool
log debugLog log debugLog
verifyFetcher verifyFetcher
} }
func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error { func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error {
@ -90,19 +99,20 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error {
if err != nil { if err != nil {
return fmt.Errorf("constructing Rekor client: %w", err) 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 { if err := cfm.flags.parse(cmd.Flags()); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
cfm.log.Debugf("Using flags %+v", cfm.flags) cfm.log.Debugf("Using flags %+v", cfm.flags)
fetcher := attestationconfigapi.NewFetcherWithClient(http.DefaultClient, constants.CDNRepositoryURL) 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( func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
cmd *cobra.Command, newCosignVerifier cosignVerifierConstructor, rekor rekorVerifier, cmd *cobra.Command, fileHandler file.Handler, fetcher attestationconfigapi.Fetcher,
fileHandler file.Handler, fetcher attestationconfigapi.Fetcher, client *http.Client,
) error { ) error {
if !cfm.canFetchMeasurements { 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.") 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 { if err := cfm.flags.updateURLs(conf); err != nil {
return err return err
} }
fetchedMeasurements, err := cfm.verifyFetcher.FetchAndVerifyMeasurements(ctx, conf.Image, conf.GetProvider(),
cfm.log.Debugf("Fetching and verifying measurements") conf.GetAttestationConfig().GetVariant(), cfm.flags.insecure)
imageVersion, err := versionsapi.NewVersionFromShortPath(conf.Image, versionsapi.VersionKindImage)
if err != nil { if err != nil {
return err var rekorErr *measurements.RekorError
} if errors.As(err, &rekorErr) {
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 {
cmd.PrintErrf("Ignoring Rekor related error: %v\n", err) cmd.PrintErrf("Ignoring Rekor related error: %v\n", err)
cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!") 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) cfm.log.Debugf("Measurements:\n", fetchedMeasurements)
@ -234,5 +202,3 @@ type rekorVerifier interface {
SearchByHash(context.Context, string) ([]string, error) SearchByHash(context.Context, string) ([]string, error)
VerifyEntry(context.Context, string, string) error VerifyEntry(context.Context, string, string) error
} }
type cosignVerifierConstructor func([]byte) (sigstore.Verifier, error)

View File

@ -7,23 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only
package cmd package cmd
import ( import (
"bytes"
"context" "context"
"fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi" "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/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -154,114 +151,17 @@ func newTestClient(fn roundTripFunc) *http.Client {
} }
func TestConfigFetchMeasurements(t *testing.T) { 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 { testCases := map[string]struct {
cosign cosignVerifierConstructor
rekor rekorVerifier
insecureFlag bool insecureFlag bool
err error
wantErr bool wantErr bool
}{ }{
"success": { "no error succeeds": {},
cosign: newStubCosignVerifier, "failing rekor verify should not result in error": {
rekor: singleUUIDVerifier(), err: &measurements.RekorError{},
}, },
"success without cosign": { "error other than Rekor fails": {
insecureFlag: true, err: assert.AnError,
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(),
wantErr: true, wantErr: true,
}, },
} }
@ -279,11 +179,12 @@ func TestConfigFetchMeasurements(t *testing.T) {
err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll) err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll)
require.NoError(err) 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.insecure = tc.insecureFlag
cfm.flags.force = true cfm.flags.force = true
err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, stubAttestationFetcher{}, client) err = cfm.configFetchMeasurements(cmd, fileHandler, stubAttestationFetcher{})
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
return 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{} type stubAttestationFetcher struct{}
func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) { func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) {

View File

@ -8,8 +8,6 @@ package cmd
import ( import (
"context" "context"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
) )
// singleUUIDVerifier constructs a RekorVerifier that returns a single UUID and no errors, // singleUUIDVerifier constructs a RekorVerifier that returns a single UUID and no errors,
@ -43,10 +41,6 @@ type stubCosignVerifier struct {
verifyError error verifyError error
} }
func newStubCosignVerifier(_ []byte) (sigstore.Verifier, error) {
return &stubCosignVerifier{}, nil
}
func (v *stubCosignVerifier) VerifySignature(_, _ []byte) error { func (v *stubCosignVerifier) VerifySignature(_, _ []byte) error {
return v.verifyError return v.verifyError
} }

View File

@ -26,7 +26,7 @@ var ErrNoVersionsFound = errors.New("no versions found")
type Fetcher interface { type Fetcher interface {
FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionAPI) (SEVSNPVersionAPI, error) FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionAPI) (SEVSNPVersionAPI, error)
FetchSEVSNPVersionList(ctx context.Context, list SEVSNPVersionList) (SEVSNPVersionList, 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. // fetcher fetches AttestationCfg API resources without authentication.

View File

@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "measurements", name = "measurements",
srcs = [ srcs = [
"fetchmeasurements.go",
"measurements.go", "measurements.go",
# keep # keep
"measurements_enterprise.go", "measurements_enterprise.go",
@ -16,6 +17,8 @@ go_library(
"//internal/api/versionsapi", "//internal/api/versionsapi",
"//internal/attestation/variant", "//internal/attestation/variant",
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/sigstore",
"//internal/sigstore/keyselect",
"@com_github_google_go_tpm//tpmutil", "@com_github_google_go_tpm//tpmutil",
"@com_github_siderolabs_talos_pkg_machinery//config/encoder", "@com_github_siderolabs_talos_pkg_machinery//config/encoder",
"@in_gopkg_yaml_v3//:yaml_v3", "@in_gopkg_yaml_v3//:yaml_v3",
@ -24,7 +27,10 @@ go_library(
go_test( go_test(
name = "measurements_test", name = "measurements_test",
srcs = ["measurements_test.go"], srcs = [
"fetchmeasurements_test.go",
"measurements_test.go",
],
embed = [":measurements"], embed = [":measurements"],
deps = [ deps = [
"//internal/api/versionsapi", "//internal/api/versionsapi",

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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 generated by tfplugindocs -->
## 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))
<a id="nestedatt--attestation"></a>
### 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)
<a id="nestedatt--attestation--azure_firmware_signer_config"></a>
### Nested Schema for `attestation.azure_firmware_signer_config`
Read-Only:
- `accepted_key_digests` (List of String)
- `enforcement_policy` (String)
- `maa_url` (String)
<a id="nestedatt--measurements"></a>
### Nested Schema for `measurements`
Read-Only:
- `expected` (String)
- `warn_only` (Boolean)

View File

@ -0,0 +1,5 @@
data "constellation_attestation" "test" {
csp = "aws"
attestation_variant = "aws-sev-snp"
image_version = "v2.13.0"
}

View File

@ -21,6 +21,7 @@ require (
github.com/agext/levenshtein v1.2.2 // indirect github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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 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/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.27 // 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/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-containerregistry v0.15.2 // 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/google/uuid v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0 // indirect
github.com/hashicorp/errwrap v1.1.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/sassoftware/relic v7.2.1+incompatible // indirect
github.com/schollz/progressbar/v3 v3.13.1 // indirect github.com/schollz/progressbar/v3 v3.13.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // 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/rekor v1.2.2 // indirect
github.com/sigstore/sigstore v1.7.1 // indirect github.com/sigstore/sigstore v1.7.1 // indirect
github.com/spf13/afero v1.10.0 // indirect github.com/spf13/afero v1.10.0 // indirect

View File

@ -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-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 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 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 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 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= 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-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 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE=
github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= 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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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= 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-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-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.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.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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 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-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-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.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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.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= 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/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-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.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=

View File

@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "provider", name = "provider",
srcs = [ srcs = [
"attestation_data_source.go",
"example_resource.go", "example_resource.go",
"image_data_source.go", "image_data_source.go",
"provider.go", "provider.go",
@ -11,9 +12,13 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/provider", importpath = "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/provider",
visibility = ["//terraform-provider-constellation:__subpackages__"], visibility = ["//terraform-provider-constellation:__subpackages__"],
deps = [ deps = [
"//internal/api/attestationconfigapi",
"//internal/attestation/measurements",
"//internal/attestation/variant", "//internal/attestation/variant",
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/config",
"//internal/imagefetcher", "//internal/imagefetcher",
"//internal/sigstore",
"//terraform-provider-constellation/internal/data", "//terraform-provider-constellation/internal/data",
"@com_github_hashicorp_terraform_plugin_framework//datasource", "@com_github_hashicorp_terraform_plugin_framework//datasource",
"@com_github_hashicorp_terraform_plugin_framework//datasource/schema", "@com_github_hashicorp_terraform_plugin_framework//datasource/schema",
@ -35,6 +40,7 @@ go_library(
go_test( go_test(
name = "provider_test", name = "provider_test",
srcs = [ srcs = [
"attestation_data_source_test.go",
"image_data_source_test.go", "image_data_source_test.go",
"provider_test.go", "provider_test.go",
], ],

View File

@ -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"`
}

View File

@ -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)
})
}
}

View File

@ -160,8 +160,6 @@ func (d *ImageDataSource) Read(ctx context.Context, req datasource.ReadRequest,
// Save data into Terraform state // Save data into Terraform state
data.Reference = types.StringValue(imageRef) 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)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
} }

View File

@ -85,6 +85,6 @@ func (p *ConstellationProvider) Resources(_ context.Context) []func() resource.R
// DataSources lists the data sources implemented by the provider. // DataSources lists the data sources implemented by the provider.
func (p *ConstellationProvider) DataSources(_ context.Context) []func() datasource.DataSource { func (p *ConstellationProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{ return []func() datasource.DataSource{
NewImageDataSource, NewImageDataSource, NewAttestationDataSource,
} }
} }