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 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
;;

View File

@ -170,7 +170,6 @@ go_test(
"//internal/license",
"//internal/logger",
"//internal/semver",
"//internal/sigstore",
"//internal/state",
"//internal/versions",
"//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/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)

View File

@ -7,23 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only
package cmd
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"testing"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -154,114 +151,17 @@ func newTestClient(fn roundTripFunc) *http.Client {
}
func TestConfigFetchMeasurements(t *testing.T) {
measurements := `{
"version": "v999.999.999",
"ref": "-",
"stream": "stable",
"list": [
{
"csp": "GCP",
"attestationVariant":"gcp-sev-es",
"measurements": {
"0": {
"expected": "0000000000000000000000000000000000000000000000000000000000000000",
"warnOnly":false
},
"1": {
"expected": "1111111111111111111111111111111111111111111111111111111111111111",
"warnOnly":false
},
"2": {
"expected": "2222222222222222222222222222222222222222222222222222222222222222",
"warnOnly":false
},
"3": {
"expected": "3333333333333333333333333333333333333333333333333333333333333333",
"warnOnly":false
},
"4": {
"expected": "4444444444444444444444444444444444444444444444444444444444444444",
"warnOnly":false
},
"5": {
"expected": "5555555555555555555555555555555555555555555555555555555555555555",
"warnOnly":false
},
"6": {
"expected": "6666666666666666666666666666666666666666666666666666666666666666",
"warnOnly":false
}
}
}
]
}
`
signature := "placeholder-signature"
client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(measurements)),
Header: make(http.Header),
}
}
if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json.sig" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(signature)),
Header: make(http.Header),
}
}
fmt.Println("unexpected request", req.URL.String())
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(bytes.NewBufferString("Not found.")),
Header: make(http.Header),
}
})
testCases := map[string]struct {
cosign cosignVerifierConstructor
rekor rekorVerifier
insecureFlag bool
err error
wantErr bool
}{
"success": {
cosign: newStubCosignVerifier,
rekor: singleUUIDVerifier(),
"no error succeeds": {},
"failing rekor verify should not result in error": {
err: &measurements.RekorError{},
},
"success without cosign": {
insecureFlag: true,
cosign: func(_ []byte) (sigstore.Verifier, error) {
return &stubCosignVerifier{
verifyError: assert.AnError,
}, nil
},
rekor: singleUUIDVerifier(),
},
"failing search should not result in error": {
cosign: newStubCosignVerifier,
rekor: &stubRekorVerifier{
SearchByHashUUIDs: []string{},
SearchByHashError: assert.AnError,
},
},
"failing verify should not result in error": {
cosign: newStubCosignVerifier,
rekor: &stubRekorVerifier{
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
VerifyEntryError: assert.AnError,
},
},
"signature verification failure": {
cosign: func(_ []byte) (sigstore.Verifier, error) {
return &stubCosignVerifier{
verifyError: assert.AnError,
}, nil
},
rekor: singleUUIDVerifier(),
"error other than Rekor fails": {
err: assert.AnError,
wantErr: true,
},
}
@ -279,11 +179,12 @@ func TestConfigFetchMeasurements(t *testing.T) {
err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll)
require.NoError(err)
cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t)}
fetcher := stubVerifyFetcher{err: tc.err}
cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t), verifyFetcher: fetcher}
cfm.flags.insecure = tc.insecureFlag
cfm.flags.force = true
err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, stubAttestationFetcher{}, client)
err = cfm.configFetchMeasurements(cmd, fileHandler, stubAttestationFetcher{})
if tc.wantErr {
assert.Error(err)
return
@ -293,6 +194,14 @@ func TestConfigFetchMeasurements(t *testing.T) {
}
}
type stubVerifyFetcher struct {
err error
}
func (f stubVerifyFetcher) FetchAndVerifyMeasurements(_ context.Context, _ string, _ cloudprovider.Provider, _ variant.Variant, _ bool) (measurements.M, error) {
return nil, f.err
}
type stubAttestationFetcher struct{}
func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) {

View File

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

View File

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

View File

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

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/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

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-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=

View File

@ -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",
],

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

View File

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