mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-09 14:39:38 -05:00
a2de1d23ec
Signed-off-by: Daniel Weiße <dw@edgeless.systems> Co-authored-by: Daniel Weiße <dw@edgeless.systems>
307 lines
11 KiB
Go
307 lines
11 KiB
Go
/*
|
|
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"`
|
|
}
|