diff --git a/bazel/ci/terraform.sh.in b/bazel/ci/terraform.sh.in
index 6d94da9842..7f355b8156 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 9b5a8ca95f..78491129d1 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 ccaa13a91f..705076846a 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 7f4aff0763..9cebbb7dac 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(),
- },
- "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,
- },
+ "no error succeeds": {},
+ "failing rekor verify should not result in error": {
+ err: &measurements.RekorError{},
},
- "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 1011b23ca6..b55c0ab15b 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 490edd3f5c..a54e3ebc7c 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 07660450d2..b3b615e19c 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 0000000000..7720d1a590
--- /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 0000000000..d79a77a411
--- /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 0000000000..fb11df2ac4
--- /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 0000000000..4b40c0a69c
--- /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 64c97bba62..93c3686ebe 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 1eb6470769..35f496c5ba 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 c44b25c829..53e8bba321 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 0000000000..eeced12f97
--- /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 0000000000..21341c0ed4
--- /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 0f41055bcf..52fb09ac96 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 9ba864756f..fdf4967332 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,
}
}