attestation: add Azure TDX attestation (#2827)

* Implement Azure TDX attestation primitives
* Add default measurements and claims for Azure TDX
* Enable Constellation on Azure TDX

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2024-01-24 15:10:15 +01:00 committed by GitHub
parent e07ea4b40f
commit e350ca0f57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 1604 additions and 681 deletions

View File

@ -2270,6 +2270,14 @@ def go_dependencies():
sum = "h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=",
version = "v0.6.0",
)
go_repository(
name = "com_github_google_go_configfs_tsm",
build_file_generation = "on",
build_file_proto_mode = "disable_global",
importpath = "github.com/google/go-configfs-tsm",
sum = "h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98=",
version = "v0.2.2",
)
go_repository(
name = "com_github_google_go_containerregistry",
build_file_generation = "on",
@ -2316,8 +2324,8 @@ def go_dependencies():
build_file_generation = "on",
build_file_proto_mode = "disable_global",
importpath = "github.com/google/go-tdx-guest",
sum = "h1:lRlUusuieEuqljjihCXb+Mr73VNitOYPJYWXzJKtBWs=",
version = "v0.2.3-0.20231011100059-4cf02bed9d33",
sum = "h1:fwe2ROo/XIpC3JWdD5My75NP1oYCQFc6wCITLP1vXCE=",
version = "v0.2.3-0.20240122115010-59d674a08de0",
)
go_repository(
name = "com_github_google_go_tpm",
@ -2339,8 +2347,8 @@ def go_dependencies():
"//3rdparty/bazel/com_github_google_go_tpm_tools:ms_tpm_20_ref.patch",
"//3rdparty/bazel/com_github_google_go_tpm_tools:include.patch",
],
sum = "h1:iyaCPKt2N5Rd0yz0G8ANa022SgCNZkMpp+db6QELtvI=",
version = "v0.4.2",
sum = "h1:EQ1rGgyI8IEBApvDH9HPF7ehUd/6H6SxSNKVDF5z/GU=",
version = "v0.4.3-0.20240112165732-912a43636883",
)
go_repository(
name = "com_github_google_go_tspi",

View File

@ -15,6 +15,7 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/libvirt"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"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"
@ -89,7 +90,9 @@ func (a *Applier) Plan(ctx context.Context, conf *config.Config) (bool, error) {
}
// Apply applies the prepared configuration by creating or updating cloud resources.
func (a *Applier) Apply(ctx context.Context, csp cloudprovider.Provider, withRollback RollbackBehavior) (infra state.Infrastructure, retErr error) {
func (a *Applier) Apply(
ctx context.Context, csp cloudprovider.Provider, attestation variant.Variant, withRollback RollbackBehavior,
) (infra state.Infrastructure, retErr error) {
if withRollback {
var rollbacker rollbacker
switch csp {
@ -105,7 +108,7 @@ func (a *Applier) Apply(ctx context.Context, csp cloudprovider.Provider, withRol
if err != nil {
return infraState, fmt.Errorf("terraform apply: %w", err)
}
if csp == cloudprovider.Azure && infraState.Azure != nil {
if csp == cloudprovider.Azure && attestation.Equal(variant.AzureSEVSNP{}) && infraState.Azure != nil {
if err := a.policyPatcher.Patch(ctx, infraState.Azure.AttestationURL); err != nil {
return infraState, fmt.Errorf("patching policies: %w", err)
}

View File

@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"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"
@ -192,7 +193,7 @@ func TestApplier(t *testing.T) {
}
assert.False(diff)
idFile, err := applier.Apply(context.Background(), tc.provider, true)
idFile, err := applier.Apply(context.Background(), tc.provider, tc.config.GetAttestationConfig().GetVariant(), true)
if tc.wantErr {
assert.Error(err)
@ -352,7 +353,7 @@ func TestApply(t *testing.T) {
out: io.Discard,
}
_, err := u.Apply(context.Background(), cloudprovider.QEMU, WithoutRollbackOnError)
_, err := u.Apply(context.Background(), cloudprovider.QEMU, variant.QEMUVTPM{}, WithoutRollbackOnError)
if tc.wantErr {
assert.Error(err)
} else {

View File

@ -158,7 +158,7 @@ func azureTerraformVars(conf *config.Config, imageRef string) (*terraform.AzureC
Location: conf.Provider.Azure.Location,
CreateMAA: toPtr(conf.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})),
Debug: toPtr(conf.IsDebugCluster()),
ConfidentialVM: toPtr(conf.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})),
ConfidentialVM: toPtr(!conf.GetAttestationConfig().GetVariant().Equal(variant.AzureTrustedLaunch{})),
SecureBoot: conf.Provider.Azure.SecureBoot,
UserAssignedIdentity: conf.Provider.Azure.UserAssignedIdentity,
ResourceGroup: conf.Provider.Azure.ResourceGroup,

View File

@ -465,9 +465,9 @@ func (a *applyCmd) validateInputs(cmd *cobra.Command, configFetcher attestationc
// a user may still end up skipping phases that could result in errors later on.
// However, we perform basic steps, like ensuring init phase is not skipped if
a.log.Debugf("Validating state file")
preCreateValidateErr := stateFile.Validate(state.PreCreate, conf.GetProvider())
preInitValidateErr := stateFile.Validate(state.PreInit, conf.GetProvider())
postInitValidateErr := stateFile.Validate(state.PostInit, conf.GetProvider())
preCreateValidateErr := stateFile.Validate(state.PreCreate, conf.GetAttestationConfig().GetVariant())
preInitValidateErr := stateFile.Validate(state.PreInit, conf.GetAttestationConfig().GetVariant())
postInitValidateErr := stateFile.Validate(state.PostInit, conf.GetAttestationConfig().GetVariant())
// If the state file is in a pre-create state, we need to create the cluster,
// in which case the workspace has to be clean

View File

@ -13,6 +13,7 @@ import (
"path/filepath"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"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"
@ -94,7 +95,8 @@ func (a *applyCmd) applyTerraformChanges(
return state.Infrastructure{}, err
}
return a.applyTerraformChangesWithMessage(
cmd, conf.GetProvider(), cloudcmd.WithRollbackOnError, terraformClient, upgradeDir,
cmd, conf.GetProvider(), conf.GetAttestationConfig().GetVariant(),
cloudcmd.WithRollbackOnError, terraformClient, upgradeDir,
"Do you want to create this cluster?",
"The creation of the cluster was aborted.",
"cluster creation aborted by user",
@ -105,7 +107,8 @@ func (a *applyCmd) applyTerraformChanges(
cmd.Println("Changes of Constellation cloud resources are required by applying an updated Terraform template.")
return a.applyTerraformChangesWithMessage(
cmd, conf.GetProvider(), cloudcmd.WithoutRollbackOnError, terraformClient, upgradeDir,
cmd, conf.GetProvider(), conf.GetAttestationConfig().GetVariant(),
cloudcmd.WithoutRollbackOnError, terraformClient, upgradeDir,
"Do you want to apply these Terraform changes?",
"Aborting upgrade.",
"cluster upgrade aborted by user",
@ -119,8 +122,8 @@ func (a *applyCmd) applyTerraformChanges(
}
func (a *applyCmd) applyTerraformChangesWithMessage(
cmd *cobra.Command, csp cloudprovider.Provider, rollbackBehavior cloudcmd.RollbackBehavior,
terraformClient cloudApplier, upgradeDir string,
cmd *cobra.Command, csp cloudprovider.Provider, attestation variant.Variant,
rollbackBehavior cloudcmd.RollbackBehavior, terraformClient cloudApplier, upgradeDir string,
confirmationQst, abortMsg, abortErrorMsg, progressMsg, successMsg string,
) (state.Infrastructure, error) {
// Ask for confirmation first
@ -146,7 +149,7 @@ func (a *applyCmd) applyTerraformChangesWithMessage(
a.log.Debugf("Applying Terraform changes")
a.spinner.Start(progressMsg, false)
infraState, err := terraformClient.Apply(cmd.Context(), csp, rollbackBehavior)
infraState, err := terraformClient.Apply(cmd.Context(), csp, attestation, rollbackBehavior)
a.spinner.Stop()
if err != nil {
return state.Infrastructure{}, fmt.Errorf("applying terraform changes: %w", err)

View File

@ -11,6 +11,7 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
"github.com/edgelesssys/constellation/v2/internal/config"
@ -19,7 +20,7 @@ import (
type cloudApplier interface {
Plan(ctx context.Context, conf *config.Config) (bool, error)
Apply(ctx context.Context, csp cloudprovider.Provider, rollback cloudcmd.RollbackBehavior) (state.Infrastructure, error)
Apply(ctx context.Context, csp cloudprovider.Provider, variant variant.Variant, rollback cloudcmd.RollbackBehavior) (state.Infrastructure, error)
RestoreWorkspace() error
WorkingDirIsEmpty() (bool, error)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
"github.com/edgelesssys/constellation/v2/internal/config"
@ -44,7 +45,7 @@ func (c *stubCloudCreator) Plan(_ context.Context, _ *config.Config) (bool, erro
return c.planDiff, c.planErr
}
func (c *stubCloudCreator) Apply(_ context.Context, _ cloudprovider.Provider, _ cloudcmd.RollbackBehavior) (state.Infrastructure, error) {
func (c *stubCloudCreator) Apply(_ context.Context, _ cloudprovider.Provider, _ variant.Variant, _ cloudcmd.RollbackBehavior) (state.Infrastructure, error) {
c.applyCalled = true
return c.state, c.applyErr
}

View File

@ -152,6 +152,8 @@ func createConfigWithAttestationVariant(provider cloudprovider.Provider, rawProv
return nil, fmt.Errorf("provider %s does not support attestation variant %s", provider, attestationVariant)
}
conf.SetAttestation(attestationVariant)
conf.SetCSPNodeGroupDefaults(provider)
return conf, nil
}

View File

@ -109,9 +109,8 @@ func (r *recoverCmd) recover(
return err
}
provider := conf.GetProvider()
r.log.Debugf("Got provider %s", provider.String())
if provider == cloudprovider.Azure {
r.log.Debugf("Got provider %s", conf.GetProvider())
if conf.GetProvider() == cloudprovider.Azure {
interval = 20 * time.Second // Azure LB takes a while to remove unhealthy instances
}
@ -119,7 +118,7 @@ func (r *recoverCmd) recover(
if err != nil {
return fmt.Errorf("reading state file: %w", err)
}
if err := stateFile.Validate(state.PostInit, provider); err != nil {
if err := stateFile.Validate(state.PostInit, conf.GetAttestationConfig().GetVariant()); err != nil {
return fmt.Errorf("validating state file: %w", err)
}

View File

@ -335,7 +335,7 @@ func (u stubTerraformUpgrader) Plan(_ context.Context, _ *config.Config) (bool,
return u.terraformDiff, u.planTerraformErr
}
func (u stubTerraformUpgrader) Apply(_ context.Context, _ cloudprovider.Provider, _ cloudcmd.RollbackBehavior) (state.Infrastructure, error) {
func (u stubTerraformUpgrader) Apply(_ context.Context, _ cloudprovider.Provider, _ variant.Variant, _ cloudcmd.RollbackBehavior) (state.Infrastructure, error) {
return state.Infrastructure{}, u.applyTerraformErr
}
@ -356,8 +356,8 @@ func (m *mockTerraformUpgrader) Plan(ctx context.Context, conf *config.Config) (
return args.Bool(0), args.Error(1)
}
func (m *mockTerraformUpgrader) Apply(ctx context.Context, provider cloudprovider.Provider, rollback cloudcmd.RollbackBehavior) (state.Infrastructure, error) {
args := m.Called(ctx, provider, rollback)
func (m *mockTerraformUpgrader) Apply(ctx context.Context, provider cloudprovider.Provider, variant variant.Variant, rollback cloudcmd.RollbackBehavior) (state.Infrastructure, error) {
args := m.Called(ctx, provider, variant, rollback)
return args.Get(0).(state.Infrastructure), args.Error(1)
}

View File

@ -30,7 +30,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"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/constellation/state"
@ -108,9 +107,9 @@ func runVerify(cmd *cobra.Command, _ []string) error {
dialer: dialer.New(nil, nil, &net.Dialer{}),
log: log,
}
formatterFactory := func(output string, provider cloudprovider.Provider, log debugLog) (attestationDocFormatter, error) {
if output == "json" && (provider != cloudprovider.Azure && provider != cloudprovider.AWS) {
return nil, errors.New("json output is only supported for Azure and AWS")
formatterFactory := func(output string, attestation variant.Variant, log debugLog) (attestationDocFormatter, error) {
if output == "json" && (!attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{})) {
return nil, errors.New("json output is only supported for Azure SEV-SNP and AWS SEV-SNP")
}
switch output {
case "json":
@ -135,7 +134,7 @@ func runVerify(cmd *cobra.Command, _ []string) error {
return v.verify(cmd, verifyClient, formatterFactory, fetcher)
}
type formatterFactory func(output string, provider cloudprovider.Provider, log debugLog) (attestationDocFormatter, error)
type formatterFactory func(output string, attestation variant.Variant, log debugLog) (attestationDocFormatter, error)
func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factory formatterFactory, configFetcher attestationconfigapi.Fetcher) error {
c.log.Debugf("Loading configuration file from %q", c.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename))
@ -152,7 +151,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor
if err != nil {
return fmt.Errorf("reading state file: %w", err)
}
if err := stateFile.Validate(state.PostInit, conf.GetProvider()); err != nil {
if err := stateFile.Validate(state.PostInit, conf.GetAttestationConfig().GetVariant()); err != nil {
return fmt.Errorf("validating state file: %w", err)
}
@ -201,15 +200,15 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor
return fmt.Errorf("verifying: %w", err)
}
// certificates are only available for Azure
formatter, err := factory(c.flags.output, conf.GetProvider(), c.log)
// certificates are only available for Azure SEV-SNP and AWS SEV-SNP
formatter, err := factory(c.flags.output, conf.GetAttestationConfig().GetVariant(), c.log)
if err != nil {
return fmt.Errorf("creating formatter: %w", err)
}
attDocOutput, err := formatter.format(
cmd.Context(),
rawAttestationDoc,
(conf.Provider.Azure == nil && conf.Provider.AWS == nil),
(!attConfig.GetVariant().Equal(variant.AzureSEVSNP{}) && !attConfig.GetVariant().Equal(variant.AWSSEVSNP{})),
attConfig,
)
if err != nil {
@ -467,7 +466,10 @@ func updateInitMeasurements(config config.AttestationCfg, ownerID, clusterID str
m := config.GetMeasurements()
switch config.GetVariant() {
case variant.AWSNitroTPM{}, variant.AWSSEVSNP{}, variant.AzureTrustedLaunch{}, variant.AzureSEVSNP{}, variant.GCPSEVES{}, variant.QEMUVTPM{}:
case variant.AWSNitroTPM{}, variant.AWSSEVSNP{},
variant.AzureTrustedLaunch{}, variant.AzureSEVSNP{}, variant.AzureTDX{}, // AzureTDX also uses a vTPM for measurements
variant.GCPSEVES{},
variant.QEMUVTPM{}:
if err := updateMeasurementTPM(m, uint32(measurements.PCRIndexOwnerID), ownerID); err != nil {
return err
}

View File

@ -216,7 +216,7 @@ func TestVerify(t *testing.T) {
endpoint: tc.nodeEndpointFlag,
},
}
formatterFac := func(_ string, _ cloudprovider.Provider, _ debugLog) (attestationDocFormatter, error) {
formatterFac := func(_ string, _ variant.Variant, _ debugLog) (attestationDocFormatter, error) {
return tc.formatter, nil
}
err := v.verify(cmd, tc.protoClient, formatterFac, stubAttestationFetcher{})

View File

@ -44,7 +44,7 @@ go_test(
deps = [
"//internal/cloud/cloudprovider",
"//internal/constants",
"//internal/constellation/state",
"//internal/encoding",
"//internal/file",
"//internal/role",
"@com_github_azure_azure_sdk_for_go_sdk_azcore//to",

View File

@ -18,7 +18,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
"github.com/edgelesssys/constellation/v2/internal/encoding"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/role"
"github.com/hashicorp/terraform-exec/tfexec"
@ -490,7 +490,7 @@ func TestCreateCluster(t *testing.T) {
}
assert.NoError(err)
assert.Equal("192.0.2.100", infraState.ClusterEndpoint)
assert.Equal(state.HexBytes("initSecret"), infraState.InitSecret)
assert.Equal(encoding.HexBytes("initSecret"), infraState.InitSecret)
assert.Equal("12345abc", infraState.UID)
assert.Equal("192.0.2.101", infraState.InClusterEndpoint)
assert.Equal("192.0.2.103/32", infraState.IPCidrNode)

View File

@ -78,7 +78,7 @@ constellation config generate {aws|azure|gcp|openstack|qemu|stackit} [flags]
### Options
```
-a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-trustedlaunch|gcp-sev-es|qemu-vtpm}. If not specified, the default for the cloud provider is used
-a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-tdx|azure-trustedlaunch|gcp-sev-es|qemu-vtpm}. If not specified, the default for the cloud provider is used
-h, --help help for generate
-k, --kubernetes string Kubernetes version to use in format MAJOR.MINOR (default "v1.28")
```

5
go.mod
View File

@ -85,8 +85,9 @@ require (
github.com/go-playground/validator/v10 v10.14.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/google/go-sev-guest v0.9.3
github.com/google/go-tdx-guest v0.2.3-0.20240122115010-59d674a08de0
github.com/google/go-tpm v0.9.0
github.com/google/go-tpm-tools v0.4.2
github.com/google/go-tpm-tools v0.4.3-0.20240112165732-912a43636883
github.com/google/uuid v1.4.0
github.com/googleapis/gax-go/v2 v2.12.0
github.com/gophercloud/gophercloud v1.5.0
@ -257,8 +258,8 @@ require (
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-attestation v0.5.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-configfs-tsm v0.2.2 // indirect
github.com/google/go-containerregistry v0.15.2 // indirect
github.com/google/go-tdx-guest v0.2.3-0.20231011100059-4cf02bed9d33 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/logger v1.1.1 // indirect

10
go.sum
View File

@ -539,14 +539,16 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98=
github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo=
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-sev-guest v0.0.0-20230928233922-2dcbba0a4b9d h1:6o4Z/vQqNUH+cEagfx1Ez5ElK70iZulEXZwmLnRo44I=
github.com/google/go-sev-guest v0.0.0-20230928233922-2dcbba0a4b9d/go.mod h1:hc1R4R6f8+NcJwITs0L90fYWTsBpd1Ix+Gur15sqHDs=
github.com/google/go-tdx-guest v0.2.3-0.20231011100059-4cf02bed9d33 h1:lRlUusuieEuqljjihCXb+Mr73VNitOYPJYWXzJKtBWs=
github.com/google/go-tdx-guest v0.2.3-0.20231011100059-4cf02bed9d33/go.mod h1:84ut3oago/BqPXD4ppiGXdkZNW3WFPkcyAO4my2hXdY=
github.com/google/go-tpm-tools v0.4.2 h1:iyaCPKt2N5Rd0yz0G8ANa022SgCNZkMpp+db6QELtvI=
github.com/google/go-tpm-tools v0.4.2/go.mod h1:fGUDZu4tw3V4hUVuFHmiYgRd0c58/IXivn9v3Ea/ck4=
github.com/google/go-tdx-guest v0.2.3-0.20240122115010-59d674a08de0 h1:fwe2ROo/XIpC3JWdD5My75NP1oYCQFc6wCITLP1vXCE=
github.com/google/go-tdx-guest v0.2.3-0.20240122115010-59d674a08de0/go.mod h1:/rc3d7rnPykOPuY8U9saMyEps0PZDThLk/RygXm04nE=
github.com/google/go-tpm-tools v0.4.3-0.20240112165732-912a43636883 h1:EQ1rGgyI8IEBApvDH9HPF7ehUd/6H6SxSNKVDF5z/GU=
github.com/google/go-tpm-tools v0.4.3-0.20240112165732-912a43636883/go.mod h1:n1reZGzBZc7VGW4/FanBgZFE5upVr58j0359Sevu32U=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

View File

@ -1,8 +1,27 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "azure",
srcs = ["azure.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure",
visibility = ["//:__subpackages__"],
deps = [
"@com_github_google_go_tpm//legacy/tpm2",
"@com_github_google_go_tpm_tools//client",
],
)
go_test(
name = "azure_test",
srcs = ["azure_test.go"],
embed = [":azure"],
deps = [
"//internal/attestation/simulator",
"//internal/attestation/snp",
"@com_github_google_go_tpm//legacy/tpm2",
"@com_github_google_go_tpm_tools//client",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)

View File

@ -13,8 +13,104 @@ Constellation supports multiple attestation technologies on Azure.
TPM attestation verified using an SEV-SNP attestation statement.
- TDX - Trust Domain Extensions
TPM attestation verified using a TDX attestation statement.
- Trusted Launch
Basic TPM attestation.
*/
package azure
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/legacy/tpm2"
)
const (
// tpmAkIdx is the NV index of the attestation key used by Azure VMs.
tpmAkIdx = 0x81000003
)
// GetAttestationKey reads the attestation key put into the TPM during early boot.
func GetAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) {
ak, err := tpmclient.LoadCachedKey(tpm, tpmAkIdx, tpmclient.NullSession{})
if err != nil {
return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err)
}
return ak, nil
}
// HCLAkValidator validates an attestation key issued by the Host Compatibility Layer (HCL).
// The HCL is written by Azure, and sits between the Hypervisor and CVM OS.
// The HCL runs in the protected context of the CVM.
type HCLAkValidator struct{}
// Validate validates that the attestation key from the TPM is trustworthy. The steps are:
// 1. runtime data read from the TPM has the same sha256 digest as reported in `report_data` of the SNP report or `TdQuoteBody.ReportData` of the TDX report.
// 2. modulus reported in runtime data matches modulus from key at idx 0x81000003.
// 3. exponent reported in runtime data matches exponent from key at idx 0x81000003.
// The function is currently tested manually on a Azure Ubuntu CVM.
func (a *HCLAkValidator) Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error {
var rtData runtimeData
if err := json.Unmarshal(runtimeDataRaw, &rtData); err != nil {
return fmt.Errorf("unmarshalling json: %w", err)
}
sum := sha256.Sum256(runtimeDataRaw)
if len(reportData) < len(sum) {
return fmt.Errorf("reportData has unexpected size: %d", len(reportData))
}
if !bytes.Equal(sum[:], reportData[:len(sum)]) {
return errors.New("unexpected runtimeData digest in TPM")
}
if len(rtData.PublicPart) < 1 {
return errors.New("did not receive any keys in runtime data")
}
rawN, err := base64.RawURLEncoding.DecodeString(rtData.PublicPart[0].N)
if err != nil {
return fmt.Errorf("decoding modulus string: %w", err)
}
if !bytes.Equal(rawN, rsaParameters.ModulusRaw) {
return fmt.Errorf("unexpected modulus value in TPM")
}
rawE, err := base64.RawURLEncoding.DecodeString(rtData.PublicPart[0].E)
if err != nil {
return fmt.Errorf("decoding exponent string: %w", err)
}
paddedRawE := make([]byte, 4)
copy(paddedRawE, rawE)
exponent := binary.LittleEndian.Uint32(paddedRawE)
// According to this comment [1] the TPM uses "0" to represent the default exponent "65537".
// The go tpm library also reports the exponent as 0. Thus we have to handle it specially.
// [1] https://github.com/tpm2-software/tpm2-tools/pull/1973#issue-596685005
if !((exponent == 65537 && rsaParameters.ExponentRaw == 0) || exponent == rsaParameters.ExponentRaw) {
return fmt.Errorf("unexpected N value in TPM")
}
return nil
}
type runtimeData struct {
PublicPart []akPub `json:"keys"`
}
// akPub are the public parameters of an RSA attestation key.
type akPub struct {
E string `json:"e"`
N string `json:"n"`
}

View File

@ -0,0 +1,165 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package azure
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"os"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/google/go-tpm-tools/client"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestValidateAk tests the attestation key validation with a simulated TPM device.
func TestValidateAk(t *testing.T) {
cgo := os.Getenv("CGO_ENABLED")
if cgo == "0" {
t.Skip("skipping test because CGO is disabled and tpm simulator requires it")
}
int32ToBytes := func(val uint32) []byte {
r := make([]byte, 4)
binary.PutUvarint(r, uint64(val))
return r
}
require := require.New(t)
tpm, err := simulator.OpenSimulatedTPM()
require.NoError(err)
defer tpm.Close()
key, err := client.AttestationKeyRSA(tpm)
require.NoError(err)
defer key.Close()
e := base64.RawURLEncoding.EncodeToString(int32ToBytes(key.PublicArea().RSAParameters.ExponentRaw))
n := base64.RawURLEncoding.EncodeToString(key.PublicArea().RSAParameters.ModulusRaw)
ak := akPub{E: e, N: n}
rtData := runtimeData{PublicPart: []akPub{ak}}
defaultRuntimeDataRaw, err := json.Marshal(rtData)
require.NoError(err)
defaultInstanceInfo := snp.InstanceInfo{Azure: &snp.AzureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}}
sig := sha256.Sum256(defaultRuntimeDataRaw)
defaultReportData := sig[:]
defaultRsaParams := key.PublicArea().RSAParameters
testCases := map[string]struct {
instanceInfo snp.InstanceInfo
runtimeDataRaw []byte
reportData []byte
rsaParameters *tpm2.RSAParams
wantErr bool
}{
"success": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: defaultReportData,
rsaParameters: defaultRsaParams,
},
"invalid json": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: []byte(""),
reportData: defaultReportData,
rsaParameters: defaultRsaParams,
wantErr: true,
},
"invalid hash": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: bytes.Repeat([]byte{0}, 64),
rsaParameters: defaultRsaParams,
wantErr: true,
},
"invalid E": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: defaultReportData,
rsaParameters: func() *tpm2.RSAParams {
tmp := *defaultRsaParams
tmp.ExponentRaw = 1
return &tmp
}(),
wantErr: true,
},
"invalid N": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: defaultReportData,
rsaParameters: func() *tpm2.RSAParams {
tmp := *defaultRsaParams
tmp.ModulusRaw = []byte{0, 1, 2, 3}
return &tmp
}(),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ak := HCLAkValidator{}
err = ak.Validate(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
// TestGetHCLAttestationKey is a basic smoke test that only checks if GetAttestationKey can be run error free.
// Testing anything else will only verify that the simulator works as expected, since GetAttestationKey
// only retrieves the attestation key from the TPM.
func TestGetHCLAttestationKey(t *testing.T) {
cgo := os.Getenv("CGO_ENABLED")
if cgo == "0" {
t.Skip("skipping test because CGO is disabled and tpm simulator requires it")
}
require := require.New(t)
assert := assert.New(t)
tpm, err := simulator.OpenSimulatedTPM()
require.NoError(err)
defer tpm.Close()
// we should receive an error if no key was saved at index `tpmAkIdx`
_, err = GetAttestationKey(tpm)
assert.Error(err)
// create a key at the index
tpmAk, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign,
RSAParameters: &tpm2.RSAParams{
Sign: &tpm2.SigScheme{
Alg: tpm2.AlgRSASSA,
Hash: tpm2.AlgSHA256,
},
KeyBits: 2048,
},
}, tpmAkIdx)
require.NoError(err)
defer tpmAk.Close()
// we should now be able to retrieve the key
_, err = GetAttestationKey(tpm)
assert.NoError(err)
}

View File

@ -14,6 +14,7 @@ go_library(
visibility = ["//:__subpackages__"],
deps = [
"//internal/attestation",
"//internal/attestation/azure",
"//internal/attestation/idkeydigest",
"//internal/attestation/snp",
"//internal/attestation/variant",
@ -28,7 +29,6 @@ go_library(
"@com_github_google_go_sev_guest//verify",
"@com_github_google_go_sev_guest//verify/trust",
"@com_github_google_go_tpm//legacy/tpm2",
"@com_github_google_go_tpm_tools//client",
"@com_github_google_go_tpm_tools//proto/attest",
],
)

View File

@ -13,15 +13,13 @@ import (
"io"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/go-azguestattestation/maa"
tpmclient "github.com/google/go-tpm-tools/client"
)
const tpmAkIdx = 0x81000003
// Issuer for Azure TPM attestation.
type Issuer struct {
variant.AzureSEVSNP
@ -40,7 +38,7 @@ func NewIssuer(log attestation.Logger) *Issuer {
i.Issuer = vtpm.NewIssuer(
vtpm.OpenVTPM,
getAttestationKey,
azure.GetAttestationKey,
i.getInstanceInfo,
log,
)
@ -83,16 +81,6 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us
return statement, nil
}
// getAttestationKey reads the attestation key put into the TPM during early boot.
func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) {
ak, err := tpmclient.LoadCachedKey(tpm, tpmAkIdx, tpmclient.NullSession{})
if err != nil {
return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err)
}
return ak, nil
}
type imdsAPI interface {
getMAAURL(ctx context.Context) (string, error)
}

View File

@ -11,14 +11,10 @@ import (
"encoding/json"
"errors"
"io"
"os"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/go-azguestattestation/maa"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -113,46 +109,6 @@ func TestGetSNPAttestation(t *testing.T) {
}
}
// TestGetHCLAttestationKey is a basic smoke test that only checks if getAkPub can be run error free.
// Testing anything else will only verify that the simulator works as expected, since getAkPub
// only retrieves the attestation key from the TPM.
func TestGetHCLAttestationKey(t *testing.T) {
cgo := os.Getenv("CGO_ENABLED")
if cgo == "0" {
t.Skip("skipping test because CGO is disabled and tpm simulator requires it")
}
require := require.New(t)
assert := assert.New(t)
tpm, err := simulator.OpenSimulatedTPM()
require.NoError(err)
defer tpm.Close()
// we should receive an error if no key was saved at index `tpmAkIdx`
_, err = getAttestationKey(tpm)
assert.Error(err)
// create a key at the index
tpmAk, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign,
RSAParameters: &tpm2.RSAParams{
Sign: &tpm2.SigScheme{
Alg: tpm2.AlgRSASSA,
Hash: tpm2.AlgSHA256,
},
KeyBits: 2048,
},
}, tpmAkIdx)
require.NoError(err)
defer tpmAk.Close()
// we should now be able to retrieve the key
_, err = getAttestationKey(tpm)
assert.NoError(err)
}
type stubImdsClient struct {
maaURL string
apiError error

View File

@ -10,15 +10,13 @@ import (
"bytes"
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
@ -78,7 +76,7 @@ func NewValidator(cfg *config.AzureSEVSNP, log attestation.Logger) *Validator {
log = nopAttestationLogger{}
}
v := &Validator{
hclValidator: &attestationKey{},
hclValidator: &azure.HCLAkValidator{},
maa: newMAAClient(),
config: cfg,
log: log,
@ -191,7 +189,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo
if err != nil {
return nil, err
}
if err = v.hclValidator.validate(instanceInfo.Azure.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil {
if err = v.hclValidator.Validate(instanceInfo.Azure.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil {
return nil, fmt.Errorf("validating HCLAkPub: %w", err)
}
@ -238,70 +236,6 @@ func (v *Validator) checkIDKeyDigest(ctx context.Context, report *spb.Attestatio
return nil
}
type attestationKey struct {
PublicPart []akPub `json:"keys"`
}
// validate validates that the attestation key from the TPM is trustworthy. The steps are:
// 1. runtime data read from the TPM has the same sha256 digest as reported in `report_data` of the SNP report.
// 2. modulus reported in runtime data matches modulus from key at idx 0x81000003.
// 3. exponent reported in runtime data matches exponent from key at idx 0x81000003.
// The function is currently tested manually on a Azure Ubuntu CVM.
func (a *attestationKey) validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error {
if err := json.Unmarshal(runtimeDataRaw, a); err != nil {
return fmt.Errorf("unmarshalling json: %w", err)
}
sum := sha256.Sum256(runtimeDataRaw)
if len(reportData) < len(sum) {
return fmt.Errorf("reportData has unexpected size: %d", len(reportData))
}
if !bytes.Equal(sum[:], reportData[:len(sum)]) {
return errors.New("unexpected runtimeData digest in TPM")
}
if len(a.PublicPart) < 1 {
return errors.New("did not receive any keys in runtime data")
}
rawN, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].N)
if err != nil {
return fmt.Errorf("decoding modulus string: %w", err)
}
if !bytes.Equal(rawN, rsaParameters.ModulusRaw) {
return fmt.Errorf("unexpected modulus value in TPM")
}
rawE, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].E)
if err != nil {
return fmt.Errorf("decoding exponent string: %w", err)
}
paddedRawE := make([]byte, 4)
copy(paddedRawE, rawE)
exponent := binary.LittleEndian.Uint32(paddedRawE)
// According to this comment [1] the TPM uses "0" to represent the default exponent "65537".
// The go tpm library also reports the exponent as 0. Thus we have to handle it specially.
// [1] https://github.com/tpm2-software/tpm2-tools/pull/1973#issue-596685005
if !((exponent == 65537 && rsaParameters.ExponentRaw == 0) || exponent == rsaParameters.ExponentRaw) {
return fmt.Errorf("unexpected N value in TPM")
}
return nil
}
// hclAkValidator validates an attestation key issued by the Host Compatibility Layer (HCL).
// The HCL is written by Azure, and sits between the Hypervisor and CVM OS.
// The HCL runs in the protected context of the CVM.
type hclAkValidator interface {
validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error
}
// akPub are the public parameters of an RSA attestation key.
type akPub struct {
E string `json:"e"`
N string `json:"n"`
}
// nopAttestationLogger is a no-op implementation of AttestationLogger.
type nopAttestationLogger struct{}
@ -314,3 +248,7 @@ func (nopAttestationLogger) Warnf(string, ...interface{}) {}
type maaValidator interface {
validateToken(ctx context.Context, maaURL string, token string, extraData []byte) error
}
type hclAkValidator interface {
Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error
}

View File

@ -10,8 +10,6 @@ import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
@ -23,7 +21,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/config"
@ -203,107 +200,6 @@ func (v *stubMaaValidator) validateToken(_ context.Context, _ string, _ string,
return v.validateTokenErr
}
// TestValidateAk tests the attestation key validation with a simulated TPM device.
func TestValidateAk(t *testing.T) {
cgo := os.Getenv("CGO_ENABLED")
if cgo == "0" {
t.Skip("skipping test because CGO is disabled and tpm simulator requires it")
}
int32ToBytes := func(val uint32) []byte {
r := make([]byte, 4)
binary.PutUvarint(r, uint64(val))
return r
}
require := require.New(t)
tpm, err := simulator.OpenSimulatedTPM()
require.NoError(err)
defer tpm.Close()
key, err := client.AttestationKeyRSA(tpm)
require.NoError(err)
defer key.Close()
e := base64.RawURLEncoding.EncodeToString(int32ToBytes(key.PublicArea().RSAParameters.ExponentRaw))
n := base64.RawURLEncoding.EncodeToString(key.PublicArea().RSAParameters.ModulusRaw)
ak := akPub{E: e, N: n}
runtimeData := attestationKey{PublicPart: []akPub{ak}}
defaultRuntimeDataRaw, err := json.Marshal(runtimeData)
require.NoError(err)
defaultInstanceInfo := snp.InstanceInfo{Azure: &snp.AzureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}}
sig := sha256.Sum256(defaultRuntimeDataRaw)
defaultReportData := sig[:]
defaultRsaParams := key.PublicArea().RSAParameters
testCases := map[string]struct {
instanceInfo snp.InstanceInfo
runtimeDataRaw []byte
reportData []byte
rsaParameters *tpm2.RSAParams
wantErr bool
}{
"success": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: defaultReportData,
rsaParameters: defaultRsaParams,
},
"invalid json": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: []byte(""),
reportData: defaultReportData,
rsaParameters: defaultRsaParams,
wantErr: true,
},
"invalid hash": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: bytes.Repeat([]byte{0}, 64),
rsaParameters: defaultRsaParams,
wantErr: true,
},
"invalid E": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: defaultReportData,
rsaParameters: func() *tpm2.RSAParams {
tmp := *defaultRsaParams
tmp.ExponentRaw = 1
return &tmp
}(),
wantErr: true,
},
"invalid N": {
instanceInfo: defaultInstanceInfo,
runtimeDataRaw: defaultRuntimeDataRaw,
reportData: defaultReportData,
rsaParameters: func() *tpm2.RSAParams {
tmp := *defaultRsaParams
tmp.ModulusRaw = []byte{0, 1, 2, 3}
return &tmp
}(),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ak := attestationKey{}
err = ak.validate(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
// TestGetTrustedKey tests the verification and validation of attestation report.
func TestTrustedKeyFromSNP(t *testing.T) {
cgo := os.Getenv("CGO_ENABLED")
@ -824,11 +720,9 @@ func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (st
}, nil
}
type stubAttestationKey struct {
PublicPart []akPub
}
type stubAttestationKey struct{}
func (s *stubAttestationKey) validate(runtimeDataRaw []byte, reportData []byte, _ *tpm2.RSAParams) error {
func (s *stubAttestationKey) Validate(runtimeDataRaw []byte, reportData []byte, _ *tpm2.RSAParams) error {
if err := json.Unmarshal(runtimeDataRaw, s); err != nil {
return fmt.Errorf("unmarshalling json: %w", err)
}

View File

@ -0,0 +1,38 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "tdx",
srcs = [
"issuer.go",
"tdx.go",
"validator.go",
],
importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx",
visibility = ["//:__subpackages__"],
deps = [
"//internal/attestation",
"//internal/attestation/azure",
"//internal/attestation/variant",
"//internal/attestation/vtpm",
"//internal/config",
"@com_github_google_go_tdx_guest//abi",
"@com_github_google_go_tdx_guest//proto/tdx",
"@com_github_google_go_tdx_guest//validate",
"@com_github_google_go_tdx_guest//verify",
"@com_github_google_go_tdx_guest//verify/trust",
"@com_github_google_go_tpm//legacy/tpm2",
"@com_github_google_go_tpm_tools//proto/attest",
],
)
go_test(
name = "tdx_test",
srcs = ["issuer_test.go"],
embed = [":tdx"],
deps = [
"//internal/attestation/azure/tdx/testdata",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)

View File

@ -0,0 +1,179 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package tdx
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/google/go-tpm/legacy/tpm2"
)
const (
imdsURL = "http://169.254.169.254/acc/tdquote"
indexHCLReport = 0x1400001
hclDataOffset = 1216
hclReportTypeOffset = 8
hclReportTypeOffsetStart = hclDataOffset + hclReportTypeOffset
hclRequestDataSizeOffset = 16
runtimeDataSizeOffset = hclDataOffset + hclRequestDataSizeOffset
hclRequestDataOffset = 20
runtimeDataOffset = hclDataOffset + hclRequestDataOffset
tdReportSize = 1024
hwReportStart = 32
hwReportEnd = 1216
)
const (
hclReportTypeInvalid uint32 = iota
hclReportTypeReserved
hclReportTypeSNP
hclReportTypeTVM
hclReportTypeTDX
)
// Issuer for Azure confidential VM attestation using TDX.
type Issuer struct {
variant.AzureTDX
*vtpm.Issuer
quoteGetter quoteGetter
}
// NewIssuer initializes a new Azure Issuer.
func NewIssuer(log attestation.Logger) *Issuer {
i := &Issuer{
quoteGetter: imdsQuoteGetter{
client: &http.Client{Transport: &http.Transport{Proxy: nil}},
},
}
i.Issuer = vtpm.NewIssuer(
vtpm.OpenVTPM,
azure.GetAttestationKey,
i.getInstanceInfo,
log,
)
return i
}
func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) {
// Read HCL report from TPM
report, err := tpm2.NVReadEx(tpm, indexHCLReport, tpm2.HandleOwner, "", 0)
if err != nil {
return nil, err
}
// Parse the report from the TPM
hwReport, runtimeData, err := parseHCLReport(report)
if err != nil {
return nil, fmt.Errorf("getting HCL report: %w", err)
}
// Get quote from IMDS API
quote, err := i.quoteGetter.getQuote(ctx, hwReport)
if err != nil {
return nil, fmt.Errorf("getting quote: %w", err)
}
instanceInfo := instanceInfo{
AttestationReport: quote,
RuntimeData: runtimeData,
}
instanceInfoJSON, err := json.Marshal(instanceInfo)
if err != nil {
return nil, fmt.Errorf("marshalling instance info: %w", err)
}
return instanceInfoJSON, nil
}
func parseHCLReport(report []byte) (hwReport, runtimeData []byte, err error) {
// First, ensure the extracted report is actually for TDX
if len(report) < hclReportTypeOffsetStart+4 {
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read HCL report type, got %d", hclReportTypeOffsetStart+4, len(report))
}
reportType := binary.LittleEndian.Uint32(report[hclReportTypeOffsetStart : hclReportTypeOffsetStart+4])
if reportType != hclReportTypeTDX {
return nil, nil, fmt.Errorf("invalid HCL report type: expected TDX (%d), got %d", hclReportTypeTDX, reportType)
}
// We need the td report (generally called HW report in Azure's samples) from the HCL report to send to the IMDS API
if len(report) < hwReportStart+tdReportSize {
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read td report, got %d", hwReportStart+tdReportSize, len(report))
}
hwReport = report[hwReportStart : hwReportStart+tdReportSize]
// We also need the runtime data to verify the attestation key later on the validator side
if len(report) < runtimeDataSizeOffset+4 {
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read runtime data size, got %d", runtimeDataSizeOffset+4, len(report))
}
runtimeDataSize := int(binary.LittleEndian.Uint32(report[runtimeDataSizeOffset : runtimeDataSizeOffset+4]))
if len(report) < runtimeDataOffset+runtimeDataSize {
return nil, nil, fmt.Errorf("invalid HCL report: expected at least %d bytes to read runtime data, got %d", runtimeDataOffset+runtimeDataSize, len(report))
}
runtimeData = report[runtimeDataOffset : runtimeDataOffset+runtimeDataSize]
return hwReport, runtimeData, nil
}
// imdsQuoteGetter issues TDX quotes using Azure's IMDS API.
type imdsQuoteGetter struct {
client *http.Client
}
func (i imdsQuoteGetter) getQuote(ctx context.Context, hwReport []byte) ([]byte, error) {
encodedReportJSON, err := json.Marshal(quoteRequest{
Report: base64.RawURLEncoding.EncodeToString(hwReport),
})
if err != nil {
return nil, fmt.Errorf("marshalling encoded report: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, imdsURL, bytes.NewReader(encodedReportJSON))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Add("Content-Type", "application/json")
res, err := i.client.Do(req)
if err != nil {
return nil, fmt.Errorf("sending request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
var quoteRes quoteResponse
if err := json.NewDecoder(res.Body).Decode(&quoteRes); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return base64.RawURLEncoding.DecodeString(quoteRes.Quote)
}
type quoteRequest struct {
Report string `json:"report"`
}
type quoteResponse struct {
Quote string `json:"quote"`
}
type quoteGetter interface {
getQuote(ctx context.Context, encodedHWReport []byte) ([]byte, error)
}

View File

@ -0,0 +1,159 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package tdx
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"io"
"net/http"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx/testdata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseHCLReport(t *testing.T) {
testCases := map[string]struct {
report []byte
wantErr bool
}{
"success using testdata": {
report: testdata.HCLReport,
wantErr: false,
},
"invalid report type": {
report: func() []byte {
report := make([]byte, len(testdata.HCLReport))
copy(report, testdata.HCLReport)
binary.LittleEndian.PutUint32(report[hclReportTypeOffsetStart:], hclReportTypeInvalid)
return report
}(),
wantErr: true,
},
"report too short for HCL report type": {
report: func() []byte {
report := make([]byte, hclReportTypeOffsetStart+3)
copy(report, testdata.HCLReport)
return report
}(),
wantErr: true,
},
"report too short for runtime data size": {
report: func() []byte {
report := make([]byte, runtimeDataSizeOffset+3)
copy(report, testdata.HCLReport)
return report
}(),
wantErr: true,
},
"runtime data shorter than runtime data size": {
report: func() []byte {
report := make([]byte, len(testdata.HCLReport))
copy(report, testdata.HCLReport)
// Lets claim the report contains a much larger runtime data entry than it actually does.
// That way, we can easily test if our code correctly handles reports that are shorter than
// what they claim to be and avoid panics.
binary.LittleEndian.PutUint32(report[runtimeDataSizeOffset:], 0xFFFF)
return report
}(),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
hwReport, runtimeData, err := parseHCLReport(tc.report)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.report[hwReportStart:hwReportStart+tdReportSize], hwReport)
assert.Greater(len(runtimeData), 0)
assert.Equal(tc.report[runtimeDataOffset:runtimeDataOffset+len(runtimeData)], runtimeData)
})
}
}
func TestIMDSGetQuote(t *testing.T) {
testCases := map[string]struct {
client *http.Client
wantErr bool
}{
"success": {
client: newTestClient(func(req *http.Request) *http.Response {
quote := quoteResponse{
Quote: "test",
}
b, err := json.Marshal(quote)
require.NoError(t, err)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(b)),
}
},
),
wantErr: false,
},
"bad status code": {
client: newTestClient(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(bytes.NewBufferString("")),
}
},
),
wantErr: true,
},
"bad json": {
client: newTestClient(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString("")),
}
},
),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
quoteGetter := imdsQuoteGetter{
client: tc.client,
}
_, err := quoteGetter.getQuote(context.Background(), []byte("test"))
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
type roundTripFunc func(req *http.Request) *http.Response
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
// newTestClient returns *http.Client with Transport replaced to avoid making real calls.
func newTestClient(fn roundTripFunc) *http.Client {
return &http.Client{
Transport: fn,
}
}

View File

@ -0,0 +1,25 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
package tdx implements attestation for TDX on Azure.
Quotes are generated using an Azure provided vTPM and the IMDS API.
They are verified using the go-tdx-guest library.
More specifically:
- The vTPM is used to collected a TPM attestation and a Hardware Compatibility Layer (HCL) report.
- The HCL report is sent to the IMDS API to generate a TDX quote.
- The quote is verified using the go-tdx-guest library.
- The quote's report data can be used to verify the TPM's attestation key.
- The attestation key can be used to verify the TPM attestation.
*/
package tdx
type instanceInfo struct {
AttestationReport []byte
RuntimeData []byte
}

View File

@ -0,0 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "testdata",
srcs = ["testdata.go"],
embedsrcs = ["hclreport.bin"],
importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx/testdata",
visibility = ["//:__subpackages__"],
)

Binary file not shown.

View File

@ -0,0 +1,15 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// Package testdata contains testing data for an attestation process.
package testdata
import _ "embed"
// HCLReport is an example HCL report from an Azure TDX VM.
//
//go:embed hclreport.bin
var HCLReport []byte

View File

@ -0,0 +1,125 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package tdx
import (
"context"
"crypto"
"crypto/x509"
"encoding/json"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/google/go-tdx-guest/abi"
"github.com/google/go-tdx-guest/proto/tdx"
"github.com/google/go-tdx-guest/validate"
"github.com/google/go-tdx-guest/verify"
"github.com/google/go-tdx-guest/verify/trust"
"github.com/google/go-tpm-tools/proto/attest"
"github.com/google/go-tpm/legacy/tpm2"
)
// Validator for Azure confidential VM attestation using TDX.
type Validator struct {
variant.AzureTDX
*vtpm.Validator
cfg *config.AzureTDX
getter trust.HTTPSGetter
hclValidator hclAkValidator
}
// NewValidator returns a new Validator for Azure confidential VM attestation using TDX.
func NewValidator(cfg *config.AzureTDX, log attestation.Logger) *Validator {
v := &Validator{
cfg: cfg,
getter: trust.DefaultHTTPSGetter(),
hclValidator: &azure.HCLAkValidator{},
}
v.Validator = vtpm.NewValidator(
cfg.Measurements,
v.getTrustedTPMKey,
func(vtpm.AttestationDocument, *attest.MachineState) error {
return nil
},
log,
)
return v
}
func (v *Validator) getTrustedTPMKey(_ context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) {
var instanceInfo instanceInfo
if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil {
return nil, err
}
quotePb, err := abi.QuoteToProto(instanceInfo.AttestationReport)
if err != nil {
return nil, err
}
quote, ok := quotePb.(*tdx.QuoteV4)
if !ok {
return nil, fmt.Errorf("unexpected quote type: %T", quote)
}
if err := v.validateQuote(quote); err != nil {
return nil, err
}
// Decode the public area of the attestation key and validate its trustworthiness.
pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub)
if err != nil {
return nil, err
}
if err = v.hclValidator.Validate(instanceInfo.RuntimeData, quote.TdQuoteBody.ReportData, pubArea.RSAParameters); err != nil {
return nil, fmt.Errorf("validating HCLAkPub: %w", err)
}
return pubArea.Key()
}
func (v *Validator) validateQuote(tdxQuote *tdx.QuoteV4) error {
roots := x509.NewCertPool()
roots.AddCert((*x509.Certificate)(&v.cfg.IntelRootKey))
if err := verify.TdxQuote(tdxQuote, &verify.Options{
// TODO: Re-enable CRL checking once issues on Azure's side are resolved.
// CheckRevocations: true,
// GetCollateral: true,
TrustedRoots: roots,
Getter: v.getter,
}); err != nil {
return err
}
if err := validate.TdxQuote(tdxQuote, &validate.Options{
HeaderOptions: validate.HeaderOptions{
MinimumQeSvn: v.cfg.QESVN,
MinimumPceSvn: v.cfg.PCESVN,
QeVendorID: v.cfg.QEVendorID,
},
TdQuoteBodyOptions: validate.TdQuoteBodyOptions{
MinimumTeeTcbSvn: v.cfg.TEETCBSVN,
MrSeam: v.cfg.MRSeam,
Xfam: v.cfg.XFAM,
},
}); err != nil {
return err
}
return nil
}
type hclAkValidator interface {
Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error
}

View File

@ -12,6 +12,7 @@ go_library(
"//internal/attestation/aws/nitrotpm",
"//internal/attestation/aws/snp",
"//internal/attestation/azure/snp",
"//internal/attestation/azure/tdx",
"//internal/attestation/azure/trustedlaunch",
"//internal/attestation/gcp",
"//internal/attestation/qemu",

View File

@ -14,6 +14,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/aws/nitrotpm"
awssnp "github.com/edgelesssys/constellation/v2/internal/attestation/aws/snp"
azuresnp "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp"
azuretdx "github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/v2/internal/attestation/gcp"
"github.com/edgelesssys/constellation/v2/internal/attestation/qemu"
@ -33,6 +34,8 @@ func Issuer(attestationVariant variant.Variant, log attestation.Logger) (atls.Is
return trustedlaunch.NewIssuer(log), nil
case variant.AzureSEVSNP{}:
return azuresnp.NewIssuer(log), nil
case variant.AzureTDX{}:
return azuretdx.NewIssuer(log), nil
case variant.GCPSEVES{}:
return gcp.NewIssuer(log), nil
case variant.QEMUVTPM{}:
@ -57,6 +60,8 @@ func Validator(cfg config.AttestationCfg, log attestation.Logger) (atls.Validato
return trustedlaunch.NewValidator(cfg, log), nil
case *config.AzureSEVSNP:
return azuresnp.NewValidator(cfg, log), nil
case *config.AzureTDX:
return azuretdx.NewValidator(cfg, log), nil
case *config.GCPSEVES:
return gcp.NewValidator(cfg, log), nil
case *config.QEMUVTPM:

View File

@ -31,6 +31,9 @@ func TestIssuer(t *testing.T) {
"azure-sev-snp": {
variant: variant.AzureSEVSNP{},
},
"azure-tdx": {
variant: variant.AzureTDX{},
},
"azure-trusted-launch": {
variant: variant.AzureTrustedLaunch{},
},
@ -77,6 +80,9 @@ func TestValidator(t *testing.T) {
"azure-sev-snp": {
cfg: &config.AzureSEVSNP{},
},
"azure-tdx": {
cfg: &config.AzureTDX{},
},
"azure-trusted-launch": {
cfg: &config.AzureTrustedLaunch{},
},

View File

@ -269,6 +269,8 @@ func attestationVariantFromGoIdentifier(identifier string) (variant.Variant, err
return variant.GCPSEVES{}, nil
case "AzureSEVSNP":
return variant.AzureSEVSNP{}, nil
case "AzureTDX":
return variant.AzureTDX{}, nil
case "AzureTrustedLaunch":
return variant.AzureTrustedLaunch{}, nil
case "QEMUVTPM":

View File

@ -497,6 +497,9 @@ func DefaultsFor(provider cloudprovider.Provider, attestationVariant variant.Var
case provider == cloudprovider.Azure && attestationVariant == variant.AzureSEVSNP{}:
return azure_AzureSEVSNP.Copy()
case provider == cloudprovider.Azure && attestationVariant == variant.AzureTDX{}:
return azure_AzureTDX.Copy()
case provider == cloudprovider.Azure && attestationVariant == variant.AzureTrustedLaunch{}:
return azure_AzureTrustedLaunch.Copy()

View File

@ -19,6 +19,7 @@ var (
aws_AWSNitroTPM = M{0: {Expected: []byte{0x73, 0x7f, 0x76, 0x7a, 0x12, 0xf5, 0x4e, 0x70, 0xee, 0xcb, 0xc8, 0x68, 0x40, 0x11, 0x32, 0x3a, 0xe2, 0xfe, 0x2d, 0xd9, 0xf9, 0x07, 0x85, 0x57, 0x79, 0x69, 0xd7, 0xa2, 0x01, 0x3e, 0x8c, 0x12}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0xe0, 0x10, 0x94, 0x98, 0xe3, 0x7e, 0x90, 0x3d, 0x2b, 0x50, 0xd6, 0x9c, 0x22, 0x51, 0x9e, 0xef, 0xac, 0xcb, 0xf4, 0x76, 0xe2, 0xf3, 0x4d, 0xd7, 0x83, 0x13, 0xe4, 0xad, 0x09, 0x9c, 0x43, 0x01}, ValidationOpt: Enforce}, 6: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x1a, 0x3b, 0x43, 0x84, 0xfa, 0x33, 0x21, 0xcb, 0x97, 0x34, 0x79, 0x07, 0x96, 0xa2, 0x87, 0x6e, 0x62, 0x2e, 0x88, 0x0c, 0xa6, 0xc6, 0x67, 0x46, 0x91, 0xe5, 0x41, 0x9c, 0xaa, 0xcd, 0x2e, 0xc6}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x75, 0xb8, 0x4d, 0xea, 0x96, 0x30, 0x17, 0x2d, 0x7f, 0x97, 0x40, 0x58, 0x54, 0x5b, 0x6b, 0xea, 0x00, 0xef, 0xe6, 0x1c, 0x12, 0x78, 0x3b, 0x7a, 0x65, 0xf2, 0xf8, 0x97, 0x14, 0xa7, 0xa9, 0xc7}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}}
aws_AWSSEVSNP = M{0: {Expected: []byte{0x7b, 0x06, 0x8c, 0x0c, 0x3a, 0xc2, 0x9a, 0xfe, 0x26, 0x41, 0x34, 0x53, 0x6b, 0x9b, 0xe2, 0x6f, 0x1d, 0x4c, 0xcd, 0x57, 0x5b, 0x88, 0xd3, 0xc3, 0xce, 0xab, 0xf3, 0x6a, 0xc9, 0x9c, 0x02, 0x78}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0x4b, 0x82, 0x51, 0x65, 0x6d, 0x00, 0x21, 0x3b, 0x62, 0x6b, 0xd1, 0xd2, 0x54, 0x4a, 0xe1, 0xa3, 0x66, 0x5c, 0x8f, 0x26, 0xcd, 0x75, 0xad, 0x3d, 0x63, 0x25, 0x65, 0x49, 0x39, 0xd9, 0xac, 0x11}, ValidationOpt: Enforce}, 6: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x18, 0x74, 0xb9, 0xb0, 0x43, 0x3b, 0xca, 0xc1, 0xdf, 0xde, 0x77, 0xd2, 0x54, 0xb5, 0xa5, 0xdf, 0x01, 0x1e, 0xdd, 0x31, 0xbf, 0xe5, 0xf0, 0xbf, 0x8a, 0x4a, 0x6a, 0x96, 0x2a, 0x8c, 0x33, 0x17}, ValidationOpt: Enforce}, 11: {Expected: []byte{0xdf, 0xe9, 0xd9, 0x70, 0x87, 0x50, 0xed, 0xf3, 0x54, 0x37, 0x9a, 0xc0, 0xef, 0xbf, 0xd0, 0x33, 0x3e, 0x6a, 0xa8, 0x98, 0x06, 0x9c, 0x87, 0xeb, 0xa5, 0xa1, 0xce, 0x3f, 0x49, 0x8a, 0xf9, 0xff}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}}
azure_AzureSEVSNP = M{1: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0xbf, 0xb4, 0x03, 0xf0, 0x9f, 0x92, 0xeb, 0x36, 0x85, 0x08, 0xea, 0x98, 0x4e, 0xf2, 0x7e, 0x75, 0x88, 0x3e, 0x99, 0x82, 0xf5, 0xef, 0x4f, 0x5e, 0xe4, 0x4d, 0xda, 0xda, 0xcf, 0xf8, 0x12, 0xf3}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x1d, 0x29, 0x84, 0x84, 0x6d, 0x78, 0xba, 0x64, 0x7f, 0xa2, 0x04, 0x95, 0x19, 0x1f, 0x22, 0x77, 0x86, 0xf5, 0xc8, 0x6a, 0x1c, 0x5a, 0x43, 0x91, 0xe3, 0x41, 0x0d, 0xac, 0x87, 0x56, 0x24, 0x50}, ValidationOpt: Enforce}, 11: {Expected: []byte{0xe1, 0xd1, 0x67, 0x62, 0x03, 0xb7, 0x64, 0xb7, 0xd9, 0x6e, 0x5c, 0x82, 0x9e, 0x4c, 0x59, 0xf2, 0x1f, 0xd5, 0x8c, 0x33, 0xd2, 0x55, 0x8e, 0xd2, 0xac, 0x96, 0x77, 0x81, 0x6f, 0xbd, 0x9d, 0x6e}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}}
azure_AzureTDX = M{1: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0x4b, 0x0b, 0x24, 0xe5, 0x2f, 0x71, 0xd7, 0x8d, 0xa3, 0x61, 0xa0, 0x59, 0x81, 0x1b, 0xaf, 0x58, 0x3f, 0x2b, 0xa4, 0x84, 0x09, 0x87, 0x41, 0x30, 0xff, 0x07, 0x31, 0x1f, 0xd9, 0x9b, 0x25, 0x87}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x98, 0x51, 0x0f, 0x10, 0x5a, 0xf2, 0x6e, 0x3d, 0xb5, 0xa9, 0x07, 0x79, 0x46, 0xd3, 0x71, 0x1a, 0xcf, 0x7a, 0x71, 0x7c, 0x3c, 0x5c, 0x5d, 0xd9, 0x87, 0xa3, 0x10, 0x1f, 0x79, 0xe7, 0x02, 0xe5}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x1f, 0x0b, 0x71, 0x58, 0xc7, 0x97, 0x8e, 0x6e, 0x30, 0x77, 0x36, 0x58, 0x3d, 0x9e, 0x6a, 0xa8, 0xa6, 0x7e, 0x05, 0x18, 0x3e, 0xa7, 0xe4, 0x67, 0x3b, 0x1b, 0x11, 0x7e, 0xbc, 0x40, 0x18, 0xd6}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}}
azure_AzureTrustedLaunch M
gcp_GCPSEVES = M{1: {Expected: []byte{0x36, 0x95, 0xdc, 0xc5, 0x5e, 0x3a, 0xa3, 0x40, 0x27, 0xc2, 0x77, 0x93, 0xc8, 0x5c, 0x72, 0x3c, 0x69, 0x7d, 0x70, 0x8c, 0x42, 0xd1, 0xf7, 0x3b, 0xd6, 0xfa, 0x4f, 0x26, 0x60, 0x8a, 0x5b, 0x24}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0x3c, 0x08, 0x38, 0x3d, 0xb2, 0xff, 0x28, 0xa1, 0x53, 0x72, 0x84, 0xd8, 0xf0, 0xed, 0x61, 0x8a, 0x34, 0x59, 0x5d, 0x26, 0x18, 0xd6, 0xb0, 0x49, 0xe6, 0x1e, 0xdc, 0xc8, 0xb6, 0xa5, 0x79, 0x99}, ValidationOpt: Enforce}, 6: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x64, 0xd1, 0x46, 0x8c, 0x1a, 0x01, 0xc5, 0xa2, 0xf0, 0xe9, 0xe9, 0x26, 0xe9, 0x03, 0xda, 0xf9, 0x67, 0xb3, 0x02, 0x33, 0x8e, 0x42, 0x67, 0xf7, 0x28, 0x38, 0x73, 0x3e, 0xe7, 0xf4, 0x09, 0xfd}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x31, 0x14, 0xe8, 0x1a, 0x29, 0xff, 0x8f, 0x9c, 0xcd, 0x78, 0x60, 0x19, 0x30, 0x13, 0x0f, 0x51, 0x65, 0x88, 0xad, 0xed, 0x82, 0xe6, 0x7e, 0xb8, 0x62, 0x22, 0xc5, 0x45, 0xdd, 0x61, 0x2d, 0xd4}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}}
qemu_QEMUTDX M

View File

@ -19,7 +19,6 @@ var (
13: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength),
}
aws_AWSSEVSNP = M{
4: PlaceHolderMeasurement(PCRMeasurementLength),
8: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
@ -29,7 +28,6 @@ var (
13: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength),
}
azure_AzureSEVSNP = M{
4: PlaceHolderMeasurement(PCRMeasurementLength),
8: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
@ -39,6 +37,15 @@ var (
13: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength),
}
azure_AzureTDX = M{
4: PlaceHolderMeasurement(PCRMeasurementLength),
8: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
9: PlaceHolderMeasurement(PCRMeasurementLength),
11: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
12: PlaceHolderMeasurement(PCRMeasurementLength),
13: WithAllBytes(0x00, Enforce, PCRMeasurementLength),
uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength),
}
azure_AzureTrustedLaunch = M{
4: PlaceHolderMeasurement(PCRMeasurementLength),
8: WithAllBytes(0x00, Enforce, PCRMeasurementLength),

View File

@ -53,7 +53,7 @@ const (
var providerAttestationMapping = map[cloudprovider.Provider][]Variant{
cloudprovider.AWS: {AWSSEVSNP{}, AWSNitroTPM{}},
cloudprovider.Azure: {AzureSEVSNP{}, AzureTrustedLaunch{}},
cloudprovider.Azure: {AzureSEVSNP{}, AzureTDX{}, AzureTrustedLaunch{}},
cloudprovider.GCP: {GCPSEVES{}},
cloudprovider.QEMU: {QEMUVTPM{}},
cloudprovider.OpenStack: {QEMUVTPM{}},
@ -114,6 +114,8 @@ func FromString(oid string) (Variant, error) {
return AzureSEVSNP{}, nil
case azureTrustedLaunch:
return AzureTrustedLaunch{}, nil
case azureTDX:
return AzureTDX{}, nil
case qemuVTPM:
return QEMUVTPM{}, nil
case qemuTDX:

View File

@ -30,6 +30,7 @@ go_library(
"//internal/config/imageversion",
"//internal/config/instancetypes",
"//internal/constants",
"//internal/encoding",
"//internal/file",
"//internal/role",
"//internal/semver",

View File

@ -17,8 +17,12 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
)
// arkPEM is the PEM encoded AMD root key. Received from the AMD Key Distribution System API (KDS).
const arkPEM = `-----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`
const (
// arkPEM is the PEM encoded AMD root key certificate. Received from the AMD Key Distribution System API (KDS).
arkPEM = `-----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`
// tdxRootPEM is the PEM encoded Intel TDX root key certificate. Receieved from the Intel Provisioning Certification Service (PCS).
tdxRootPEM = `-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n`
)
// AttestationCfg is the common interface for passing attestation configs.
type AttestationCfg interface {
@ -44,6 +48,8 @@ func UnmarshalAttestationConfig(data []byte, attestVariant variant.Variant) (Att
return unmarshalTypedConfig[*AzureSEVSNP](data)
case variant.AzureTrustedLaunch{}:
return unmarshalTypedConfig[*AzureTrustedLaunch](data)
case variant.AzureTDX{}:
return unmarshalTypedConfig[*AzureTDX](data)
case variant.GCPSEVES{}:
return unmarshalTypedConfig[*GCPSEVES](data)
case variant.QEMUVTPM{}:

View File

@ -16,7 +16,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
)
var _ sevsnpMarshaller = &AWSSEVSNP{}
var _ svnResolveMarshaller = &AWSSEVSNP{}
// DefaultForAWSSEVSNP provides a valid default configuration for AWS SEV-SNP attestation.
func DefaultForAWSSEVSNP() *AWSSEVSNP {

View File

@ -15,9 +15,13 @@ import (
"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/encoding"
)
var _ sevsnpMarshaller = &AzureSEVSNP{}
var (
_ svnResolveMarshaller = &AzureSEVSNP{}
_ svnResolveMarshaller = &AzureTDX{}
)
// DefaultForAzureSEVSNP returns the default configuration for Azure SEV-SNP attestation.
// Version numbers have placeholder values and the latest available values can be fetched using [AzureSEVSNP.FetchAndSetLatestVersionNumbers].
@ -132,3 +136,49 @@ func (c AzureTrustedLaunch) EqualTo(other AttestationCfg) (bool, error) {
}
return c.Measurements.EqualTo(otherCfg.Measurements), nil
}
// DefaultForAzureTDX returns the default configuration for Azure TDX attestation.
func DefaultForAzureTDX() *AzureTDX {
return &AzureTDX{
Measurements: measurements.DefaultsFor(cloudprovider.Azure, variant.AzureTDX{}),
QESVN: 0,
PCESVN: 0,
TEETCBSVN: encoding.HexBytes{0x02, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
QEVendorID: encoding.HexBytes{0x93, 0x9a, 0x72, 0x33, 0xf7, 0x9c, 0x4c, 0xa9, 0x94, 0x0a, 0x0d, 0xb3, 0x95, 0x7f, 0x06, 0x07},
MRSeam: encoding.HexBytes{0x36, 0x03, 0x04, 0xd3, 0x4a, 0x16, 0xaa, 0xce, 0x0a, 0x18, 0xe0, 0x9a, 0xd2, 0xd0, 0x7d, 0x2b, 0x9f, 0xd3, 0xc1, 0x74, 0x37, 0x8e, 0x5b, 0xf1, 0x08, 0x38, 0x80, 0x79, 0x82, 0x7f, 0x89, 0xff, 0x62, 0xac, 0xc5, 0xf8, 0xc4, 0x73, 0xdd, 0x40, 0x70, 0x63, 0x24, 0x83, 0x4e, 0x20, 0x29, 0x46},
XFAM: encoding.HexBytes{0xe7, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00},
IntelRootKey: mustParsePEM(tdxRootPEM),
}
}
// GetVariant returns azure-tdx as the variant.
func (AzureTDX) GetVariant() variant.Variant {
return variant.AzureTDX{}
}
// GetMeasurements returns the measurements used for attestation.
func (c AzureTDX) GetMeasurements() measurements.M {
return c.Measurements
}
// SetMeasurements updates a config's measurements using the given measurements.
func (c *AzureTDX) SetMeasurements(m measurements.M) {
c.Measurements = m
}
// EqualTo returns true if the config is equal to the given config.
func (c AzureTDX) EqualTo(other AttestationCfg) (bool, error) {
otherCfg, ok := other.(*AzureTDX)
if !ok {
return false, fmt.Errorf("cannot compare %T with %T", c, other)
}
return c.Measurements.EqualTo(otherCfg.Measurements), nil
}
func (c *AzureTDX) getToMarshallLatestWithResolvedVersions() AttestationCfg {
cp := *c
// TODO: We probably want to support "latest" pseudo versioning for Azure TDX
// But we should decide on which claims can be reliably used for attestation first
return &cp
}

View File

@ -41,6 +41,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config/imageversion"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/encoding"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/semver"
"github.com/edgelesssys/constellation/v2/internal/versions"
@ -282,6 +283,9 @@ type AttestationConfig struct {
// Azure SEV-SNP attestation.\nFor details see: https://docs.edgeless.systems/constellation/architecture/attestation#cvm-verification
AzureSEVSNP *AzureSEVSNP `yaml:"azureSEVSNP,omitempty" validate:"omitempty,dive"`
// description: |
// Azure TDX attestation.
AzureTDX *AzureTDX `yaml:"azureTDX,omitempty" validate:"omitempty,dive"`
// description: |
// Azure TPM attestation (Trusted Launch).
AzureTrustedLaunch *AzureTrustedLaunch `yaml:"azureTrustedLaunch,omitempty" validate:"omitempty,dive"`
// description: |
@ -397,6 +401,7 @@ func Default() *Config {
AWSSEVSNP: DefaultForAWSSEVSNP(),
AWSNitroTPM: &AWSNitroTPM{Measurements: measurements.DefaultsFor(cloudprovider.AWS, variant.AWSNitroTPM{})},
AzureSEVSNP: DefaultForAzureSEVSNP(),
AzureTDX: DefaultForAzureTDX(),
AzureTrustedLaunch: &AzureTrustedLaunch{Measurements: measurements.DefaultsFor(cloudprovider.Azure, variant.AzureTrustedLaunch{})},
GCPSEVES: &GCPSEVES{Measurements: measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVES{})},
QEMUVTPM: &QEMUVTPM{Measurements: measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUVTPM{})},
@ -523,6 +528,9 @@ func (c *Config) UpdateMeasurements(newMeasurements measurements.M) {
if c.Attestation.AzureSEVSNP != nil {
c.Attestation.AzureSEVSNP.Measurements.CopyFrom(newMeasurements)
}
if c.Attestation.AzureTDX != nil {
c.Attestation.AzureTDX.Measurements.CopyFrom(newMeasurements)
}
if c.Attestation.AzureTrustedLaunch != nil {
c.Attestation.AzureTrustedLaunch.Measurements.CopyFrom(newMeasurements)
}
@ -561,7 +569,7 @@ func (c *Config) RemoveProviderExcept(provider cloudprovider.Provider) {
default:
c.Provider = currentProviderConfigs
}
c.setCSPNodeGroupDefaults(provider)
c.SetCSPNodeGroupDefaults(provider)
}
// SetAttestation sets the attestation config for the given attestation variant and removes all other attestation configs.
@ -569,12 +577,14 @@ func (c *Config) SetAttestation(attestation variant.Variant) {
currentAttestationConfigs := c.Attestation
c.Attestation = AttestationConfig{}
switch attestation.(type) {
case variant.AzureSEVSNP:
c.Attestation = AttestationConfig{AzureSEVSNP: currentAttestationConfigs.AzureSEVSNP}
case variant.AWSSEVSNP:
c.Attestation = AttestationConfig{AWSSEVSNP: currentAttestationConfigs.AWSSEVSNP}
case variant.AWSNitroTPM:
c.Attestation = AttestationConfig{AWSNitroTPM: currentAttestationConfigs.AWSNitroTPM}
case variant.AzureSEVSNP:
c.Attestation = AttestationConfig{AzureSEVSNP: currentAttestationConfigs.AzureSEVSNP}
case variant.AzureTDX:
c.Attestation = AttestationConfig{AzureTDX: currentAttestationConfigs.AzureTDX}
case variant.AzureTrustedLaunch:
c.Attestation = AttestationConfig{AzureTrustedLaunch: currentAttestationConfigs.AzureTrustedLaunch}
case variant.GCPSEVES:
@ -637,6 +647,9 @@ func (c *Config) GetAttestationConfig() AttestationCfg {
if c.Attestation.AzureSEVSNP != nil {
return c.Attestation.AzureSEVSNP.getToMarshallLatestWithResolvedVersions()
}
if c.Attestation.AzureTDX != nil {
return c.Attestation.AzureTDX.getToMarshallLatestWithResolvedVersions()
}
if c.Attestation.AzureTrustedLaunch != nil {
return c.Attestation.AzureTrustedLaunch
}
@ -905,7 +918,8 @@ func (c *Config) WithOpenStackProviderDefaults(openStackProvider string) *Config
return c
}
func (c *Config) setCSPNodeGroupDefaults(csp cloudprovider.Provider) {
// SetCSPNodeGroupDefaults sets the default values for the node groups based on the configured CSP.
func (c *Config) SetCSPNodeGroupDefaults(csp cloudprovider.Provider) {
var instanceType, stateDiskType, zone string
switch csp {
case cloudprovider.AWS:
@ -913,14 +927,19 @@ func (c *Config) setCSPNodeGroupDefaults(csp cloudprovider.Provider) {
stateDiskType = "gp3"
zone = c.Provider.AWS.Zone
case cloudprovider.Azure:
instanceType = "Standard_DC4as_v5"
// Check attestation variant, and use different default instance type if we have TDX
if c.GetAttestationConfig().GetVariant().Equal(variant.AzureTDX{}) {
instanceType = "Standard_DC4es_v5"
} else {
instanceType = "Standard_DC4as_v5"
}
stateDiskType = "Premium_LRS"
case cloudprovider.GCP:
instanceType = "n2d-standard-4"
stateDiskType = "pd-ssd"
zone = c.Provider.GCP.Zone
case cloudprovider.QEMU, cloudprovider.OpenStack:
// empty. There are now defaults for this CSP
// empty. There are no defaults for this CSP
}
for groupName, group := range c.NodeGroups {
@ -986,10 +1005,6 @@ func (c GCPSEVES) EqualTo(other AttestationCfg) (bool, error) {
return c.Measurements.EqualTo(otherCfg.Measurements), nil
}
func toPtr[T any](v T) *T {
return &v
}
// QEMUVTPM is the configuration for QEMU vTPM attestation.
type QEMUVTPM struct {
// description: |
@ -1119,8 +1134,40 @@ type AzureTrustedLaunch struct {
Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"`
}
// sevsnpMarshaller is used to marshall "latest" versions with resolved version numbers.
type sevsnpMarshaller interface {
// AzureTDX is the configuration for Azure TDX attestation.
type AzureTDX struct {
// description: |
// Expected TPM measurements.
Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"`
// description: |
// Minimum required QE security version number (SVN).
QESVN uint16 `json:"qeSVN" yaml:"qeSVN"`
// description: |
// Minimum required PCE security version number (SVN).
PCESVN uint16 `json:"pceSVN" yaml:"pceSVN"`
// description: |
// Component-wise minimum required 16 byte hex-encoded TEE_TCB security version number (SVN).
TEETCBSVN encoding.HexBytes `json:"teeTCBSVN" yaml:"teeTCBSVN"`
// description: |
// Expected 16 byte hex-encoded QE_VENDOR_ID field.
QEVendorID encoding.HexBytes `json:"qeVendorID" yaml:"qeVendorID"`
// description: |
// Expected 48 byte hex-encoded MR_SEAM value.
MRSeam encoding.HexBytes `json:"mrSeam" yaml:"mrSeam"`
// description: |
// Expected 8 byte hex-encoded XFAM field.
XFAM encoding.HexBytes `json:"xfam" yaml:"xfam"`
// description: |
// Intel Root Key certificate used to verify the TDX certificate chain.
IntelRootKey Certificate `json:"intelRootKey" yaml:"intelRootKey"`
}
func toPtr[T any](v T) *T {
return &v
}
// svnResolveMarshaller is used to marshall "latest" security version numbers with resolved versions.
type svnResolveMarshaller interface {
// getToMarshallLatestWithResolvedVersions brings the attestation config into a state where marshalling uses the numerical version numbers for "latest" versions.
getToMarshallLatestWithResolvedVersions() AttestationCfg
}

View File

@ -29,6 +29,7 @@ var (
AWSNitroTPMDoc encoder.Doc
AzureSEVSNPDoc encoder.Doc
AzureTrustedLaunchDoc encoder.Doc
AzureTDXDoc encoder.Doc
)
func init() {
@ -412,7 +413,7 @@ func init() {
FieldName: "attestation",
},
}
AttestationConfigDoc.Fields = make([]encoder.Doc, 7)
AttestationConfigDoc.Fields = make([]encoder.Doc, 8)
AttestationConfigDoc.Fields[0].Name = "awsSEVSNP"
AttestationConfigDoc.Fields[0].Type = "AWSSEVSNP"
AttestationConfigDoc.Fields[0].Note = ""
@ -428,26 +429,31 @@ func init() {
AttestationConfigDoc.Fields[2].Note = ""
AttestationConfigDoc.Fields[2].Description = "Azure SEV-SNP attestation.\nFor details see: https://docs.edgeless.systems/constellation/architecture/attestation#cvm-verification"
AttestationConfigDoc.Fields[2].Comments[encoder.LineComment] = "Azure SEV-SNP attestation.\nFor details see: https://docs.edgeless.systems/constellation/architecture/attestation#cvm-verification"
AttestationConfigDoc.Fields[3].Name = "azureTrustedLaunch"
AttestationConfigDoc.Fields[3].Type = "AzureTrustedLaunch"
AttestationConfigDoc.Fields[3].Name = "azureTDX"
AttestationConfigDoc.Fields[3].Type = "AzureTDX"
AttestationConfigDoc.Fields[3].Note = ""
AttestationConfigDoc.Fields[3].Description = "Azure TPM attestation (Trusted Launch)."
AttestationConfigDoc.Fields[3].Comments[encoder.LineComment] = "Azure TPM attestation (Trusted Launch)."
AttestationConfigDoc.Fields[4].Name = "gcpSEVES"
AttestationConfigDoc.Fields[4].Type = "GCPSEVES"
AttestationConfigDoc.Fields[3].Description = "Azure TDX attestation."
AttestationConfigDoc.Fields[3].Comments[encoder.LineComment] = "Azure TDX attestation."
AttestationConfigDoc.Fields[4].Name = "azureTrustedLaunch"
AttestationConfigDoc.Fields[4].Type = "AzureTrustedLaunch"
AttestationConfigDoc.Fields[4].Note = ""
AttestationConfigDoc.Fields[4].Description = "GCP SEV-ES attestation."
AttestationConfigDoc.Fields[4].Comments[encoder.LineComment] = "GCP SEV-ES attestation."
AttestationConfigDoc.Fields[5].Name = "qemuTDX"
AttestationConfigDoc.Fields[5].Type = "QEMUTDX"
AttestationConfigDoc.Fields[4].Description = "Azure TPM attestation (Trusted Launch)."
AttestationConfigDoc.Fields[4].Comments[encoder.LineComment] = "Azure TPM attestation (Trusted Launch)."
AttestationConfigDoc.Fields[5].Name = "gcpSEVES"
AttestationConfigDoc.Fields[5].Type = "GCPSEVES"
AttestationConfigDoc.Fields[5].Note = ""
AttestationConfigDoc.Fields[5].Description = "QEMU tdx attestation."
AttestationConfigDoc.Fields[5].Comments[encoder.LineComment] = "QEMU tdx attestation."
AttestationConfigDoc.Fields[6].Name = "qemuVTPM"
AttestationConfigDoc.Fields[6].Type = "QEMUVTPM"
AttestationConfigDoc.Fields[5].Description = "GCP SEV-ES attestation."
AttestationConfigDoc.Fields[5].Comments[encoder.LineComment] = "GCP SEV-ES attestation."
AttestationConfigDoc.Fields[6].Name = "qemuTDX"
AttestationConfigDoc.Fields[6].Type = "QEMUTDX"
AttestationConfigDoc.Fields[6].Note = ""
AttestationConfigDoc.Fields[6].Description = "QEMU vTPM attestation."
AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "QEMU vTPM attestation."
AttestationConfigDoc.Fields[6].Description = "QEMU tdx attestation."
AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "QEMU tdx attestation."
AttestationConfigDoc.Fields[7].Name = "qemuVTPM"
AttestationConfigDoc.Fields[7].Type = "QEMUVTPM"
AttestationConfigDoc.Fields[7].Note = ""
AttestationConfigDoc.Fields[7].Description = "QEMU vTPM attestation."
AttestationConfigDoc.Fields[7].Comments[encoder.LineComment] = "QEMU vTPM attestation."
NodeGroupDoc.Type = "NodeGroup"
NodeGroupDoc.Comments[encoder.LineComment] = "NodeGroup defines a group of nodes with the same role and configuration."
@ -697,6 +703,57 @@ func init() {
AzureTrustedLaunchDoc.Fields[0].Note = ""
AzureTrustedLaunchDoc.Fields[0].Description = "Expected TPM measurements."
AzureTrustedLaunchDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
AzureTDXDoc.Type = "AzureTDX"
AzureTDXDoc.Comments[encoder.LineComment] = "AzureTDX is the configuration for Azure TDX attestation."
AzureTDXDoc.Description = "AzureTDX is the configuration for Azure TDX attestation."
AzureTDXDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "AttestationConfig",
FieldName: "azureTDX",
},
}
AzureTDXDoc.Fields = make([]encoder.Doc, 8)
AzureTDXDoc.Fields[0].Name = "measurements"
AzureTDXDoc.Fields[0].Type = "M"
AzureTDXDoc.Fields[0].Note = ""
AzureTDXDoc.Fields[0].Description = "Expected TPM measurements."
AzureTDXDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
AzureTDXDoc.Fields[1].Name = "qeSVN"
AzureTDXDoc.Fields[1].Type = "uint16"
AzureTDXDoc.Fields[1].Note = ""
AzureTDXDoc.Fields[1].Description = "Minimum required QE security version number (SVN)."
AzureTDXDoc.Fields[1].Comments[encoder.LineComment] = "Minimum required QE security version number (SVN)."
AzureTDXDoc.Fields[2].Name = "pceSVN"
AzureTDXDoc.Fields[2].Type = "uint16"
AzureTDXDoc.Fields[2].Note = ""
AzureTDXDoc.Fields[2].Description = "Minimum required PCE security version number (SVN)."
AzureTDXDoc.Fields[2].Comments[encoder.LineComment] = "Minimum required PCE security version number (SVN)."
AzureTDXDoc.Fields[3].Name = "teeTCBSVN"
AzureTDXDoc.Fields[3].Type = "HexBytes"
AzureTDXDoc.Fields[3].Note = ""
AzureTDXDoc.Fields[3].Description = "Component-wise minimum required 16 byte hex-encoded TEE_TCB security version number (SVN)."
AzureTDXDoc.Fields[3].Comments[encoder.LineComment] = "Component-wise minimum required 16 byte hex-encoded TEE_TCB security version number (SVN)."
AzureTDXDoc.Fields[4].Name = "qeVendorID"
AzureTDXDoc.Fields[4].Type = "HexBytes"
AzureTDXDoc.Fields[4].Note = ""
AzureTDXDoc.Fields[4].Description = "Expected 16 byte hex-encoded QE_VENDOR_ID field."
AzureTDXDoc.Fields[4].Comments[encoder.LineComment] = "Expected 16 byte hex-encoded QE_VENDOR_ID field."
AzureTDXDoc.Fields[5].Name = "mrSeam"
AzureTDXDoc.Fields[5].Type = "HexBytes"
AzureTDXDoc.Fields[5].Note = ""
AzureTDXDoc.Fields[5].Description = "Expected 48 byte hex-encoded MR_SEAM value."
AzureTDXDoc.Fields[5].Comments[encoder.LineComment] = "Expected 48 byte hex-encoded MR_SEAM value."
AzureTDXDoc.Fields[6].Name = "xfam"
AzureTDXDoc.Fields[6].Type = "HexBytes"
AzureTDXDoc.Fields[6].Note = ""
AzureTDXDoc.Fields[6].Description = "Expected 8 byte hex-encoded XFAM field."
AzureTDXDoc.Fields[6].Comments[encoder.LineComment] = "Expected 8 byte hex-encoded XFAM field."
AzureTDXDoc.Fields[7].Name = "intelRootKey"
AzureTDXDoc.Fields[7].Type = "Certificate"
AzureTDXDoc.Fields[7].Note = ""
AzureTDXDoc.Fields[7].Description = "Intel Root Key certificate used to verify the TDX certificate chain."
AzureTDXDoc.Fields[7].Comments[encoder.LineComment] = "Intel Root Key certificate used to verify the TDX certificate chain."
}
func (_ Config) Doc() *encoder.Doc {
@ -771,6 +828,10 @@ func (_ AzureTrustedLaunch) Doc() *encoder.Doc {
return &AzureTrustedLaunchDoc
}
func (_ AzureTDX) Doc() *encoder.Doc {
return &AzureTDXDoc
}
// GetConfigurationDoc returns documentation for the file ./config_doc.go.
func GetConfigurationDoc() *encoder.FileDoc {
return &encoder.FileDoc{
@ -795,6 +856,7 @@ func GetConfigurationDoc() *encoder.FileDoc {
&AWSNitroTPMDoc,
&AzureSEVSNPDoc,
&AzureTrustedLaunchDoc,
&AzureTDXDoc,
},
}
}

View File

@ -328,7 +328,7 @@ func TestFromFile(t *testing.T) {
}
func TestValidate(t *testing.T) {
const defaultErrCount = 37 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const defaultErrCount = 38 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const azErrCount = 7
const awsErrCount = 8
const gcpErrCount = 8
@ -686,137 +686,95 @@ func TestConfig_IsReleaseImage(t *testing.T) {
func TestValidInstanceTypeForProvider(t *testing.T) {
testCases := map[string]struct {
provider cloudprovider.Provider
variant variant.Variant
instanceTypes []string
nonCVMsAllowed bool
expectedResult bool
}{
"empty all": {
provider: cloudprovider.Unknown,
variant: variant.Dummy{},
instanceTypes: []string{},
expectedResult: false,
},
"empty aws": {
provider: cloudprovider.AWS,
variant: variant.AWSSEVSNP{},
instanceTypes: []string{},
expectedResult: false,
},
"empty azure only CVMs": {
provider: cloudprovider.Azure,
variant: variant.AzureSEVSNP{},
instanceTypes: []string{},
expectedResult: false,
},
"empty azure with non-CVMs": {
provider: cloudprovider.Azure,
variant: variant.AzureTrustedLaunch{},
instanceTypes: []string{},
nonCVMsAllowed: true,
expectedResult: false,
},
"empty gcp": {
provider: cloudprovider.GCP,
variant: variant.GCPSEVES{},
instanceTypes: []string{},
expectedResult: false,
},
"azure only CVMs (SNP)": {
provider: cloudprovider.Azure,
variant: variant.AzureSEVSNP{},
instanceTypes: instancetypes.AzureSNPInstanceTypes,
expectedResult: true,
},
"azure only CVMs (TDX)": {
provider: cloudprovider.Azure,
variant: variant.AzureTDX{},
instanceTypes: instancetypes.AzureTDXInstanceTypes,
expectedResult: true,
},
"azure CVMs but CVMs disabled": {
provider: cloudprovider.Azure,
instanceTypes: instancetypes.AzureSNPInstanceTypes,
nonCVMsAllowed: true,
expectedResult: false,
},
"azure trusted launch VMs with CVMs enabled": {
provider: cloudprovider.Azure,
"azure trusted launch VMs": {
variant: variant.AzureTrustedLaunch{},
instanceTypes: instancetypes.AzureTrustedLaunchInstanceTypes,
expectedResult: false,
},
"azure trusted launch VMs with CVMs disabled": {
provider: cloudprovider.Azure,
instanceTypes: instancetypes.AzureTrustedLaunchInstanceTypes,
nonCVMsAllowed: true,
expectedResult: true,
},
"gcp": {
provider: cloudprovider.GCP,
variant: variant.GCPSEVES{},
instanceTypes: instancetypes.GCPInstanceTypes,
expectedResult: true,
},
"put gcp when azure is set": {
provider: cloudprovider.Azure,
variant: variant.AzureSEVSNP{},
instanceTypes: instancetypes.GCPInstanceTypes,
expectedResult: false,
},
"put gcp when azure is set with CVMs disabled": {
provider: cloudprovider.Azure,
instanceTypes: instancetypes.GCPInstanceTypes,
nonCVMsAllowed: true,
expectedResult: false,
},
"put azure when gcp is set": {
provider: cloudprovider.GCP,
variant: variant.GCPSEVES{},
instanceTypes: instancetypes.AzureSNPInstanceTypes,
expectedResult: false,
},
"put azure when gcp is set with CVMs disabled": {
provider: cloudprovider.GCP,
instanceTypes: instancetypes.AzureTrustedLaunchInstanceTypes,
nonCVMsAllowed: true,
expectedResult: false,
},
// Testing every possible instance type for AWS is not feasible, so we just test a few based on known supported / unsupported families
// Also serves as a test for checkIfInstanceInValidAWSFamilys
"aws two valid instances": {
provider: cloudprovider.AWS,
variant: variant.AWSSEVSNP{},
instanceTypes: []string{"c5.xlarge", "c5a.2xlarge", "c5a.16xlarge", "u-12tb1.112xlarge"},
expectedResult: true,
nonCVMsAllowed: true,
expectedResult: false, // False because 2 two of the instances are not valid
},
"aws one valid instance one with too little vCPUs": {
provider: cloudprovider.AWS,
variant: variant.AWSSEVSNP{},
instanceTypes: []string{"c5.medium"},
expectedResult: false,
nonCVMsAllowed: true,
},
"aws graviton sub-family unsupported": {
provider: cloudprovider.AWS,
variant: variant.AWSSEVSNP{},
instanceTypes: []string{"m6g.xlarge", "r6g.2xlarge", "x2gd.xlarge", "g5g.8xlarge"},
expectedResult: false,
nonCVMsAllowed: true,
},
"aws combined two valid instances as one string": {
provider: cloudprovider.AWS,
variant: variant.AWSSEVSNP{},
instanceTypes: []string{"c5.xlarge, c5a.2xlarge"},
expectedResult: false,
nonCVMsAllowed: true,
},
"aws only CVMs": {
provider: cloudprovider.AWS,
variant: variant.AWSSEVSNP{},
instanceTypes: []string{"c6a.xlarge", "m6a.xlarge", "r6a.xlarge"},
expectedResult: true,
},
"aws CVMs but CVMs disabled": {
provider: cloudprovider.AWS,
instanceTypes: []string{"m6a.xlarge", "c6a.xlarge", "r6a.xlarge"},
nonCVMsAllowed: true,
expectedResult: true,
},
"aws nitroTPM VMs with CVMs enabled": {
provider: cloudprovider.AWS,
"aws nitroTPM VMs": {
variant: variant.AWSNitroTPM{},
instanceTypes: []string{"c5.xlarge", "c5a.2xlarge", "c5a.16xlarge", "u-12tb1.112xlarge"},
expectedResult: false,
},
"aws nitroTPM VMs with CVMs disabled": {
provider: cloudprovider.AWS,
instanceTypes: []string{"c5.xlarge", "c5a.2xlarge", "c5a.16xlarge", "u-12tb1.112xlarge"},
nonCVMsAllowed: true,
expectedResult: true,
},
}
@ -824,7 +782,10 @@ func TestValidInstanceTypeForProvider(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
for _, instanceType := range tc.instanceTypes {
assert.Equal(tc.expectedResult, validInstanceTypeForProvider(instanceType, tc.nonCVMsAllowed, tc.provider), instanceType)
assert.Equal(
tc.expectedResult, validInstanceTypeForProvider(instanceType, tc.variant),
instanceType,
)
}
})
}

View File

@ -193,6 +193,9 @@ func validateAttestation(sl validator.StructLevel) {
if attestation.AzureSEVSNP != nil {
attestationCount++
}
if attestation.AzureTDX != nil {
attestationCount++
}
if attestation.AzureTrustedLaunch != nil {
attestationCount++
}
@ -244,7 +247,7 @@ func translateNoAttestationError(ut ut.Translator, fe validator.FieldError) stri
}
func registerNoAttestationError(ut ut.Translator) error {
return ut.Add("no_attestation", "{0}: No attestation has been defined (requires either awsSEVSNP, awsNitroTPM, azureSEVSNP, azureTrustedLaunch, gcpSEVES, or qemuVTPM)", true)
return ut.Add("no_attestation", "{0}: No attestation has been defined (requires either awsSEVSNP, awsNitroTPM, azureSEVSNP, azureTDX, azureTrustedLaunch, gcpSEVES, or qemuVTPM)", true)
}
func translateNoDefaultControlPlaneGroupError(ut ut.Translator, fe validator.FieldError) string {
@ -352,6 +355,9 @@ func (c *Config) translateMoreThanOneAttestationError(ut ut.Translator, fe valid
if c.Attestation.AzureSEVSNP != nil {
definedAttestations = append(definedAttestations, "AzureSEVSNP")
}
if c.Attestation.AzureTDX != nil {
definedAttestations = append(definedAttestations, "AzureTDX")
}
if c.Attestation.AzureTrustedLaunch != nil {
definedAttestations = append(definedAttestations, "AzureTrustedLaunch")
}
@ -508,42 +514,38 @@ func (c *Config) translateMoreThanOneProviderError(ut ut.Translator, fe validato
return t
}
func validInstanceTypeForProvider(insType string, acceptNonCVM bool, provider cloudprovider.Provider) bool {
switch provider {
case cloudprovider.AWS:
return isSupportedAWSInstanceType(insType, acceptNonCVM)
case cloudprovider.Azure:
if acceptNonCVM {
for _, instanceType := range instancetypes.AzureTrustedLaunchInstanceTypes {
if insType == instanceType {
return true
}
}
} else {
for _, instanceType := range instancetypes.AzureTDXInstanceTypes {
if insType == instanceType {
return true
}
}
for _, instanceType := range instancetypes.AzureSNPInstanceTypes {
if insType == instanceType {
return true
}
func validInstanceTypeForProvider(insType string, attestation variant.Variant) bool {
switch attestation {
case variant.AWSSEVSNP{}, variant.AWSNitroTPM{}:
return isSupportedAWSInstanceType(insType, attestation.Equal(variant.AWSNitroTPM{}))
case variant.AzureSEVSNP{}:
for _, instanceType := range instancetypes.AzureSNPInstanceTypes {
if insType == instanceType {
return true
}
}
return false
case cloudprovider.GCP:
case variant.AzureTDX{}:
for _, instanceType := range instancetypes.AzureTDXInstanceTypes {
if insType == instanceType {
return true
}
}
case variant.AzureTrustedLaunch{}:
for _, instanceType := range instancetypes.AzureTrustedLaunchInstanceTypes {
if insType == instanceType {
return true
}
}
case variant.GCPSEVES{}:
for _, instanceType := range instancetypes.GCPInstanceTypes {
if insType == instanceType {
return true
}
}
return false
case cloudprovider.OpenStack, cloudprovider.QEMU:
case variant.QEMUVTPM{}, variant.QEMUTDX{}:
return true
default:
return false
}
return false
}
// isSupportedAWSInstanceType checks if an AWS instance type passed as user input is in one of the supported instance types.
@ -781,16 +783,7 @@ func (c *Config) validateNodeGroupZoneField(fl validator.FieldLevel) bool {
}
func (c *Config) validateInstanceType(fl validator.FieldLevel) bool {
acceptNonCVM := false
if c.GetAttestationConfig().GetVariant().Equal(variant.AzureTrustedLaunch{}) {
acceptNonCVM = true
}
if c.GetAttestationConfig().GetVariant().Equal(variant.AWSNitroTPM{}) {
acceptNonCVM = true
}
return validInstanceTypeForProvider(fl.Field().String(), acceptNonCVM, c.GetProvider())
return validInstanceTypeForProvider(fl.Field().String(), c.GetAttestationConfig().GetVariant())
}
func (c *Config) validateStateDiskTypeField(fl validator.FieldLevel) bool {

View File

@ -10,7 +10,8 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/internal/constellation/state",
visibility = ["//:__subpackages__"],
deps = [
"//internal/cloud/cloudprovider",
"//internal/attestation/variant",
"//internal/encoding",
"//internal/file",
"//internal/validation",
"@cat_dario_mergo//:mergo",
@ -26,13 +27,12 @@ go_test(
],
embed = [":state"],
deps = [
"//internal/cloud/cloudprovider",
"//internal/attestation/variant",
"//internal/constants",
"//internal/file",
"@com_github_siderolabs_talos_pkg_machinery//config/encoder",
"@com_github_spf13_afero//:afero",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@in_gopkg_yaml_v3//:yaml_v3",
],
)

View File

@ -13,13 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
package state
import (
"encoding/hex"
"errors"
"fmt"
"os"
"dario.cat/mergo"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/encoding"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/validation"
)
@ -101,7 +101,7 @@ type ClusterValues struct {
OwnerID string `yaml:"ownerID"`
// description: |
// Salt used to generate the ClusterID on the bootstrapping node.
MeasurementSalt HexBytes `yaml:"measurementSalt"`
MeasurementSalt encoding.HexBytes `yaml:"measurementSalt"`
}
// Infrastructure describe the state related to the cloud resources of the cluster.
@ -118,7 +118,7 @@ type Infrastructure struct {
InClusterEndpoint string `yaml:"inClusterEndpoint"`
// description: |
// Secret used to authenticate the bootstrapping node.
InitSecret HexBytes `yaml:"initSecret"`
InitSecret encoding.HexBytes `yaml:"initSecret"`
// description: |
// List of Subject Alternative Names (SANs) to add to the Kubernetes API server certificate.
// If no SANs should be added, this field can be left empty.
@ -213,7 +213,7 @@ Validate validates the state against the given constraint set and CSP, which can
- PreInit, which is the constraint set that should be enforced before "constellation apply" is run.
- PostInit, which is the constraint set that should be enforced after "constellation apply" is run.
*/
func (s *State) Validate(constraintSet ConstraintSet, csp cloudprovider.Provider) error {
func (s *State) Validate(constraintSet ConstraintSet, variant variant.Variant) error {
v := validation.NewValidator()
switch constraintSet {
@ -223,11 +223,11 @@ func (s *State) Validate(constraintSet ConstraintSet, csp cloudprovider.Provider
})
case PreInit:
return v.Validate(s, validation.ValidateOptions{
OverrideConstraints: s.preInitConstraints,
OverrideConstraints: s.preInitConstraints(variant),
})
case PostInit:
return v.Validate(s, validation.ValidateOptions{
OverrideConstraints: s.postInitConstraints(csp),
OverrideConstraints: s.postInitConstraints(variant),
})
default:
return errors.New("unknown constraint set")
@ -282,128 +282,130 @@ func (s *State) preCreateConstraints() []*validation.Constraint {
//
// The constraints check if the infrastructure state is valid, and if the cluster values
// are empty, which is required for the cluster to initialize correctly.
func (s *State) preInitConstraints() []*validation.Constraint {
return []*validation.Constraint{
// state version needs to be accepted by the parsing CLI.
validation.OneOf(s.Version, []string{Version1}).
WithFieldTrace(s, &s.Version),
// infrastructure must be valid.
// out-of-cluster endpoint needs to be a valid DNS name or IP address.
validation.Or(
validation.DNSName(s.Infrastructure.ClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.ClusterEndpoint),
validation.IPAddress(s.Infrastructure.ClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.ClusterEndpoint),
),
// in-cluster endpoint needs to be a valid DNS name or IP address.
validation.Or(
validation.DNSName(s.Infrastructure.InClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.InClusterEndpoint),
validation.IPAddress(s.Infrastructure.InClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.InClusterEndpoint),
),
// Node IP Cidr needs to be a valid CIDR range.
validation.CIDR(s.Infrastructure.IPCidrNode).
WithFieldTrace(s, &s.Infrastructure.IPCidrNode),
// UID needs to be filled.
validation.NotEmpty(s.Infrastructure.UID).
WithFieldTrace(s, &s.Infrastructure.UID),
// Name needs to be filled.
validation.NotEmpty(s.Infrastructure.Name).
WithFieldTrace(s, &s.Infrastructure.Name),
// GCP values need to be nil, empty, or valid.
validation.Or(
func (s *State) preInitConstraints(attestation variant.Variant) func() []*validation.Constraint {
return func() []*validation.Constraint {
constraints := []*validation.Constraint{
// state version needs to be accepted by the parsing CLI.
validation.OneOf(s.Version, []string{Version1}).
WithFieldTrace(s, &s.Version),
// infrastructure must be valid.
// out-of-cluster endpoint needs to be a valid DNS name or IP address.
validation.Or(
// nil.
validation.Equal(s.Infrastructure.GCP, nil).
WithFieldTrace(s, &s.Infrastructure.GCP),
// empty.
validation.IfNotNil(
s.Infrastructure.GCP,
func() *validation.Constraint {
return validation.Empty(*s.Infrastructure.GCP).
WithFieldTrace(s, &s.Infrastructure.GCP)
},
validation.DNSName(s.Infrastructure.ClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.ClusterEndpoint),
validation.IPAddress(s.Infrastructure.ClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.ClusterEndpoint),
),
// in-cluster endpoint needs to be a valid DNS name or IP address.
validation.Or(
validation.DNSName(s.Infrastructure.InClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.InClusterEndpoint),
validation.IPAddress(s.Infrastructure.InClusterEndpoint).
WithFieldTrace(s, &s.Infrastructure.InClusterEndpoint),
),
// Node IP Cidr needs to be a valid CIDR range.
validation.CIDR(s.Infrastructure.IPCidrNode).
WithFieldTrace(s, &s.Infrastructure.IPCidrNode),
// UID needs to be filled.
validation.NotEmpty(s.Infrastructure.UID).
WithFieldTrace(s, &s.Infrastructure.UID),
// Name needs to be filled.
validation.NotEmpty(s.Infrastructure.Name).
WithFieldTrace(s, &s.Infrastructure.Name),
// ClusterValues must be empty.
// As the clusterValues struct contains slices, we cannot use the
// Empty constraint on the entire struct. Instead, we need to check
// each field individually.
validation.Empty(s.ClusterValues.ClusterID).
WithFieldTrace(s, &s.ClusterValues.ClusterID),
// ownerID is currently unused as functionality is not implemented
// Therefore, we don't want to validate it
// validation.Empty(s.ClusterValues.OwnerID).
// WithFieldTrace(s, &s.ClusterValues.OwnerID),
validation.EmptySlice(s.ClusterValues.MeasurementSalt).
WithFieldTrace(s, &s.ClusterValues.MeasurementSalt),
}
switch attestation {
case variant.AzureSEVSNP{}, variant.AzureTDX{}, variant.AzureTrustedLaunch{}:
// Azure values need to be valid after infrastructure creation.
constraints = append(constraints,
// GCP values need to be nil or empty.
validation.Or(
validation.Equal(s.Infrastructure.GCP, nil).
WithFieldTrace(s, &s.Infrastructure.GCP),
validation.IfNotNil(
s.Infrastructure.GCP,
func() *validation.Constraint {
return validation.Empty(s.Infrastructure.GCP).
WithFieldTrace(s, &s.Infrastructure.GCP)
},
),
),
),
// valid.
validation.IfNotNil(
s.Infrastructure.GCP,
func() *validation.Constraint {
return validation.And(
validation.EvaluateAll,
// ProjectID needs to be filled.
validation.NotEmpty(s.Infrastructure.GCP.ProjectID).
WithFieldTrace(s, &s.Infrastructure.GCP.ProjectID),
// Pod IP Cidr needs to be a valid CIDR range.
validation.CIDR(s.Infrastructure.GCP.IPCidrPod).
WithFieldTrace(s, &s.Infrastructure.GCP.IPCidrPod),
)
},
),
),
// Azure values need to be nil, empty, or valid.
validation.Or(
validation.Or(
// nil.
validation.Equal(s.Infrastructure.Azure, nil).
WithFieldTrace(s, &s.Infrastructure.Azure),
// empty.
validation.IfNotNil(
s.Infrastructure.Azure,
func() *validation.Constraint {
return validation.And(
validation.EvaluateAll,
validation.Empty(s.Infrastructure.Azure.ResourceGroup).
validation.NotEmpty(s.Infrastructure.Azure.ResourceGroup).
WithFieldTrace(s, &s.Infrastructure.Azure.ResourceGroup),
validation.Empty(s.Infrastructure.Azure.SubscriptionID).
validation.NotEmpty(s.Infrastructure.Azure.SubscriptionID).
WithFieldTrace(s, &s.Infrastructure.Azure.SubscriptionID),
validation.Empty(s.Infrastructure.Azure.NetworkSecurityGroupName).
validation.NotEmpty(s.Infrastructure.Azure.NetworkSecurityGroupName).
WithFieldTrace(s, &s.Infrastructure.Azure.NetworkSecurityGroupName),
validation.Empty(s.Infrastructure.Azure.LoadBalancerName).
validation.NotEmpty(s.Infrastructure.Azure.LoadBalancerName).
WithFieldTrace(s, &s.Infrastructure.Azure.LoadBalancerName),
validation.Empty(s.Infrastructure.Azure.UserAssignedIdentity).
validation.NotEmpty(s.Infrastructure.Azure.UserAssignedIdentity).
WithFieldTrace(s, &s.Infrastructure.Azure.UserAssignedIdentity),
validation.Empty(s.Infrastructure.Azure.AttestationURL).
WithFieldTrace(s, &s.Infrastructure.Azure.AttestationURL),
)
},
),
),
// valid.
validation.IfNotNil(
s.Infrastructure.Azure,
func() *validation.Constraint {
return validation.And(
validation.EvaluateAll,
validation.NotEmpty(s.Infrastructure.Azure.ResourceGroup).
WithFieldTrace(s, &s.Infrastructure.Azure.ResourceGroup),
validation.NotEmpty(s.Infrastructure.Azure.SubscriptionID).
WithFieldTrace(s, &s.Infrastructure.Azure.SubscriptionID),
validation.NotEmpty(s.Infrastructure.Azure.NetworkSecurityGroupName).
WithFieldTrace(s, &s.Infrastructure.Azure.NetworkSecurityGroupName),
validation.NotEmpty(s.Infrastructure.Azure.LoadBalancerName).
WithFieldTrace(s, &s.Infrastructure.Azure.LoadBalancerName),
validation.NotEmpty(s.Infrastructure.Azure.UserAssignedIdentity).
WithFieldTrace(s, &s.Infrastructure.Azure.UserAssignedIdentity),
validation.NotEmpty(s.Infrastructure.Azure.AttestationURL).
WithFieldTrace(s, &s.Infrastructure.Azure.AttestationURL),
)
},
),
),
// ClusterValues must be empty.
// As the clusterValues struct contains slices, we cannot use the
// Empty constraint on the entire struct. Instead, we need to check
// each field individually.
validation.Empty(s.ClusterValues.ClusterID).
WithFieldTrace(s, &s.ClusterValues.ClusterID),
// ownerID is currently unused as functionality is not implemented
// Therefore, we don't want to validate it
// validation.Empty(s.ClusterValues.OwnerID).
// WithFieldTrace(s, &s.ClusterValues.OwnerID),
validation.EmptySlice(s.ClusterValues.MeasurementSalt).
WithFieldTrace(s, &s.ClusterValues.MeasurementSalt),
)
if attestation.Equal(variant.AzureSEVSNP{}) {
constraints = append(constraints,
validation.IfNotNil(
s.Infrastructure.Azure,
func() *validation.Constraint {
// For SEV-SNP attestation, we require the attestation URL to be set.
return validation.NotEmpty(s.Infrastructure.Azure.AttestationURL).
WithFieldTrace(s, &s.Infrastructure.Azure.AttestationURL)
},
),
)
}
case variant.GCPSEVES{}:
// GCP values need to be valid after infrastructure creation.
constraints = append(constraints,
// Azure values need to be nil or empty.
validation.Or(
validation.Equal(s.Infrastructure.Azure, nil).
WithFieldTrace(s, &s.Infrastructure.Azure),
validation.IfNotNil(
s.Infrastructure.Azure,
func() *validation.Constraint {
return validation.Empty(s.Infrastructure.Azure).
WithFieldTrace(s, &s.Infrastructure.Azure)
},
),
),
validation.IfNotNil(
s.Infrastructure.GCP,
func() *validation.Constraint {
return validation.And(
validation.EvaluateAll,
// ProjectID needs to be filled.
validation.NotEmpty(s.Infrastructure.GCP.ProjectID).
WithFieldTrace(s, &s.Infrastructure.GCP.ProjectID),
// Pod IP Cidr needs to be a valid CIDR range.
validation.CIDR(s.Infrastructure.GCP.IPCidrPod).
WithFieldTrace(s, &s.Infrastructure.GCP.IPCidrPod),
)
},
),
)
}
return constraints
}
}
@ -412,7 +414,7 @@ func (s *State) preInitConstraints() []*validation.Constraint {
//
// The constraints check if the infrastructure state and cluster state
// is valid, so that the cluster can be used correctly.
func (s *State) postInitConstraints(csp cloudprovider.Provider) func() []*validation.Constraint {
func (s *State) postInitConstraints(attestation variant.Variant) func() []*validation.Constraint {
return func() []*validation.Constraint {
constraints := []*validation.Constraint{
// state version needs to be accepted by the parsing CLI.
@ -455,8 +457,8 @@ func (s *State) postInitConstraints(csp cloudprovider.Provider) func() []*valida
WithFieldTrace(s, &s.ClusterValues.MeasurementSalt),
}
switch csp {
case cloudprovider.Azure:
switch attestation {
case variant.AzureSEVSNP{}, variant.AzureTDX{}, variant.AzureTrustedLaunch{}:
constraints = append(constraints,
// GCP values need to be nil or empty.
validation.Or(
@ -468,7 +470,8 @@ func (s *State) postInitConstraints(csp cloudprovider.Provider) func() []*valida
return validation.Empty(s.Infrastructure.GCP).
WithFieldTrace(s, &s.Infrastructure.GCP)
},
)),
),
),
// Azure values need to be valid.
validation.IfNotNil(
s.Infrastructure.Azure,
@ -485,13 +488,23 @@ func (s *State) postInitConstraints(csp cloudprovider.Provider) func() []*valida
WithFieldTrace(s, &s.Infrastructure.Azure.LoadBalancerName),
validation.NotEmpty(s.Infrastructure.Azure.UserAssignedIdentity).
WithFieldTrace(s, &s.Infrastructure.Azure.UserAssignedIdentity),
validation.NotEmpty(s.Infrastructure.Azure.AttestationURL).
WithFieldTrace(s, &s.Infrastructure.Azure.AttestationURL),
)
},
),
)
case cloudprovider.GCP:
if attestation.Equal(variant.AzureSEVSNP{}) {
constraints = append(constraints,
validation.IfNotNil(
s.Infrastructure.Azure,
func() *validation.Constraint {
// For SEV-SNP attestation, we require the attestation URL to be set.
return validation.NotEmpty(s.Infrastructure.Azure.AttestationURL).
WithFieldTrace(s, &s.Infrastructure.Azure.AttestationURL)
},
),
)
}
case variant.GCPSEVES{}:
constraints = append(constraints,
// Azure values need to be nil or empty.
validation.Or(
@ -503,7 +516,8 @@ func (s *State) postInitConstraints(csp cloudprovider.Provider) func() []*valida
return validation.Empty(s.Infrastructure.Azure).
WithFieldTrace(s, &s.Infrastructure.Azure)
},
)),
),
),
// GCP values need to be valid.
validation.IfNotNil(
s.Infrastructure.GCP,
@ -554,33 +568,3 @@ func (s *State) postInitConstraints(csp cloudprovider.Provider) func() []*valida
func (s *State) Constraints() []*validation.Constraint {
return []*validation.Constraint{}
}
// HexBytes is a byte slice that is marshalled to and from a hex string.
type HexBytes []byte
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (h *HexBytes) UnmarshalYAML(unmarshal func(any) error) error {
var hexString string
if err := unmarshal(&hexString); err != nil {
// TODO(msanft): Remove with v2.14.0
// fall back to unmarshalling as a byte slice for backwards compatibility
var oldHexBytes []byte
if err := unmarshal(&oldHexBytes); err != nil {
return fmt.Errorf("unmarshalling hex bytes: %w", err)
}
hexString = hex.EncodeToString(oldHexBytes)
}
bytes, err := hex.DecodeString(hexString)
if err != nil {
return fmt.Errorf("decoding hex bytes: %w", err)
}
*h = bytes
return nil
}
// MarshalYAML implements the yaml.Marshaler interface.
func (h HexBytes) MarshalYAML() (any, error) {
return hex.EncodeToString(h), nil
}

View File

@ -15,7 +15,6 @@ import (
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
// defaultState returns a valid default state for testing.
@ -327,97 +326,6 @@ func TestMerge(t *testing.T) {
}
}
func TestMarshalHexBytes(t *testing.T) {
testCases := map[string]struct {
in HexBytes
expected string
wantErr bool
}{
"success": {
in: []byte{0xab, 0xcd, 0xef},
expected: "abcdef\n",
},
"empty": {
in: []byte{},
expected: "\"\"\n",
},
"nil": {
in: nil,
expected: "\"\"\n",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
actual, err := yaml.Marshal(tc.in)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.expected, string(actual))
}
})
}
}
func TestUnmarshalHexBytes(t *testing.T) {
testCases := map[string]struct {
in string
expected HexBytes
wantErr bool
}{
"success": {
in: "abcdef",
expected: []byte{0xab, 0xcd, 0xef},
},
"empty": {
in: "",
expected: nil,
},
"byte slice compat": {
in: "[0xab, 0xcd, 0xef]",
expected: []byte{0xab, 0xcd, 0xef},
},
"byte slice compat 2": {
in: "[00, 12, 34]",
expected: []byte{0x00, 0x0c, 0x22},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
var actual HexBytes
err := yaml.Unmarshal([]byte(tc.in), &actual)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.expected, actual)
}
})
}
}
func TestMarshalUnmarshalHexBytes(t *testing.T) {
in := HexBytes{0xab, 0xcd, 0xef}
expected := "abcdef\n"
actual, err := yaml.Marshal(in)
require.NoError(t, err)
assert.Equal(t, expected, string(actual))
var actual2 HexBytes
err = yaml.Unmarshal(actual, &actual2)
require.NoError(t, err)
assert.Equal(t, in, actual2)
}
func TestCreateOrRead(t *testing.T) {
testCases := map[string]struct {
fs file.Handler

View File

@ -9,7 +9,7 @@ package state
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -70,7 +70,7 @@ func TestPreCreateValidation(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
err := tc.stateFile().Validate(PreCreate, cloudprovider.Azure)
err := tc.stateFile().Validate(PreCreate, variant.AzureSEVSNP{})
if tc.wantErr {
require.Error(t, err)
if tc.errAssertions != nil {
@ -92,6 +92,7 @@ func TestPreInitValidation(t *testing.T) {
testCases := map[string]struct {
stateFile func() *State
variant variant.Variant
wantErr bool
errAssertions func(a *assert.Assertions, err error)
}{
@ -172,6 +173,8 @@ func TestPreInitValidation(t *testing.T) {
s.Infrastructure.GCP = &GCP{}
return s
},
variant: variant.GCPSEVES{},
wantErr: true,
},
"gcp nil": {
stateFile: func() *State {
@ -179,6 +182,8 @@ func TestPreInitValidation(t *testing.T) {
s.Infrastructure.GCP = nil
return s
},
variant: variant.GCPSEVES{},
wantErr: true,
},
"gcp invalid": {
stateFile: func() *State {
@ -187,6 +192,7 @@ func TestPreInitValidation(t *testing.T) {
return s
},
wantErr: true,
variant: variant.GCPSEVES{},
errAssertions: func(a *assert.Assertions, err error) {
a.Contains(err.Error(), "validating State.infrastructure.gcp.ipCidrPod: invalid must be a valid CIDR")
},
@ -197,6 +203,8 @@ func TestPreInitValidation(t *testing.T) {
s.Infrastructure.Azure = &Azure{}
return s
},
variant: variant.AzureSEVSNP{},
wantErr: true,
},
"azure nil": {
stateFile: func() *State {
@ -204,6 +212,8 @@ func TestPreInitValidation(t *testing.T) {
s.Infrastructure.Azure = nil
return s
},
variant: variant.AzureSEVSNP{},
wantErr: true,
},
"azure invalid": {
stateFile: func() *State {
@ -215,12 +225,13 @@ func TestPreInitValidation(t *testing.T) {
errAssertions: func(a *assert.Assertions, err error) {
a.Contains(err.Error(), "validating State.infrastructure.azure.networkSecurityGroupName: must not be empty")
},
variant: variant.AzureSEVSNP{},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
err := tc.stateFile().Validate(PreInit, cloudprovider.Azure)
err := tc.stateFile().Validate(PreInit, tc.variant)
if tc.wantErr {
require.Error(t, err)
if tc.errAssertions != nil {
@ -236,13 +247,13 @@ func TestPreInitValidation(t *testing.T) {
func TestPostInitValidation(t *testing.T) {
testCases := map[string]struct {
stateFile func() *State
provider cloudprovider.Provider
variant variant.Variant
wantErr bool
errAssertions func(a *assert.Assertions, err error)
}{
"valid": {
stateFile: defaultGCPState,
provider: cloudprovider.GCP,
variant: variant.GCPSEVES{},
},
"invalid version": {
stateFile: func() *State {
@ -317,22 +328,39 @@ func TestPostInitValidation(t *testing.T) {
s := defaultGCPState()
return s
},
provider: cloudprovider.GCP,
variant: variant.GCPSEVES{},
},
"azure valid": {
stateFile: func() *State {
s := defaultAzureState()
return s
},
provider: cloudprovider.Azure,
variant: variant.AzureSEVSNP{},
},
"azure SEV needs attestation URL": {
stateFile: func() *State {
s := defaultAzureState()
s.Infrastructure.Azure.AttestationURL = ""
return s
},
variant: variant.AzureSEVSNP{},
wantErr: true,
},
"azure TDX does not need attestation URL": {
stateFile: func() *State {
s := defaultAzureState()
s.Infrastructure.Azure.AttestationURL = ""
return s
},
variant: variant.AzureTDX{},
},
"gcp, azure not nil": {
stateFile: func() *State {
s := defaultState()
return s
},
provider: cloudprovider.GCP,
wantErr: true,
variant: variant.GCPSEVES{},
wantErr: true,
errAssertions: func(a *assert.Assertions, err error) {
a.Contains(err.Error(), "must be equal to <nil>")
a.Contains(err.Error(), "must be empty")
@ -343,8 +371,8 @@ func TestPostInitValidation(t *testing.T) {
s := defaultState()
return s
},
provider: cloudprovider.Azure,
wantErr: true,
variant: variant.AzureSEVSNP{},
wantErr: true,
errAssertions: func(a *assert.Assertions, err error) {
a.Contains(err.Error(), "must be equal to <nil>")
a.Contains(err.Error(), "must be empty")
@ -365,7 +393,7 @@ func TestPostInitValidation(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
err := tc.stateFile().Validate(PostInit, tc.provider)
err := tc.stateFile().Validate(PostInit, tc.variant)
if tc.wantErr {
require.Error(t, err)
if tc.errAssertions != nil {

View File

@ -0,0 +1,20 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "encoding",
srcs = ["encoding.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/encoding",
visibility = ["//:__subpackages__"],
)
go_test(
name = "encoding_test",
srcs = ["encoding_test.go"],
embed = [":encoding"],
deps = [
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@in_gopkg_yaml_v3//:yaml_v3",
],
)

View File

@ -0,0 +1,71 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// Package encoding provides data types and functions for JSON or YAML encoding/decoding.
package encoding
import (
"encoding/hex"
"encoding/json"
"fmt"
)
// HexBytes is a byte slice that is marshalled to and from a hex string.
type HexBytes []byte
// String returns the hex encoded string representation of the byte slice.
func (h HexBytes) String() string {
return hex.EncodeToString(h)
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (h *HexBytes) UnmarshalJSON(data []byte) error {
var hexString string
if err := json.Unmarshal(data, &hexString); err != nil {
return err
}
// special case to stay consistent with yaml unmarshaler:
// on empty string, unmarshal to nil
if hexString == "" {
*h = nil
return nil
}
return h.unmarshal(hexString)
}
// MarshalJSON implements the json.Marshaler interface.
func (h HexBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(h.String())
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (h *HexBytes) UnmarshalYAML(unmarshal func(any) error) error {
var hexString string
if err := unmarshal(&hexString); err != nil {
// compatibility mode for old state file format:
// fall back to unmarshalling as a byte slice for backwards compatibility
var oldHexBytes []byte
if err := unmarshal(&oldHexBytes); err != nil {
return fmt.Errorf("unmarshalling hex bytes: %w", err)
}
hexString = hex.EncodeToString(oldHexBytes)
}
return h.unmarshal(hexString)
}
// MarshalYAML implements the yaml.Marshaler interface.
func (h HexBytes) MarshalYAML() (any, error) {
return h.String(), nil
}
func (h *HexBytes) unmarshal(hexString string) error {
bytes, err := hex.DecodeString(hexString)
if err != nil {
return fmt.Errorf("decoding hex bytes: %w", err)
}
*h = bytes
return nil
}

View File

@ -0,0 +1,136 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package encoding
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestMarshalHexBytes(t *testing.T) {
testCases := map[string]struct {
in HexBytes
expectedJSON string
expectedYAML string
wantErr bool
}{
"success": {
in: []byte{0xab, 0xcd, 0xef},
expectedJSON: "\"abcdef\"",
expectedYAML: "abcdef\n",
},
"empty": {
in: []byte{},
expectedJSON: "\"\"",
expectedYAML: "\"\"\n",
},
"nil": {
in: nil,
expectedJSON: "\"\"",
expectedYAML: "\"\"\n",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
actualYAML, errYAML := yaml.Marshal(tc.in)
actualJSON, errJSON := json.Marshal(tc.in)
if tc.wantErr {
assert.Error(errYAML)
assert.Error(errJSON)
return
}
assert.NoError(errYAML)
assert.NoError(errJSON)
assert.Equal(tc.expectedYAML, string(actualYAML), "yaml")
assert.Equal(tc.expectedJSON, string(actualJSON), "json")
})
}
}
func TestUnmarshalHexBytes(t *testing.T) {
testCases := map[string]struct {
yamlString string
jsonString string
expected HexBytes
wantErr bool
}{
"success": {
yamlString: "abcdef",
jsonString: "\"abcdef\"",
expected: []byte{0xab, 0xcd, 0xef},
},
"empty": {
yamlString: "",
jsonString: "\"\"",
expected: nil,
},
"byte slice compat": {
yamlString: "[0xab, 0xcd, 0xef]",
jsonString: "\"abcdef\"", // no backwards compatibility since we never used this format for json
expected: []byte{0xab, 0xcd, 0xef},
},
"byte slice compat 2": {
yamlString: "[00, 12, 34]",
jsonString: "\"000c22\"", // no backwards compatibility since we never used this format for json
expected: []byte{0x00, 0x0c, 0x22},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
var actualYAML HexBytes
errYAML := yaml.Unmarshal([]byte(tc.yamlString), &actualYAML)
var actualJSON HexBytes
errJSON := json.Unmarshal([]byte(tc.jsonString), &actualJSON)
if tc.wantErr {
assert.Error(errYAML)
assert.Error(errJSON)
return
}
assert.NoError(errYAML)
assert.NoError(errJSON)
assert.Equal(tc.expected, actualYAML, "yaml")
assert.Equal(tc.expected, actualJSON, "json")
})
}
}
func TestMarshalUnmarshalHexBytes(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
in := HexBytes{0xab, 0xcd, 0xef}
expectedJSON := "\"abcdef\""
expectedYAML := "abcdef\n"
actualJSON, err := json.Marshal(in)
require.NoError(err)
assert.Equal(expectedJSON, string(actualJSON))
actualYAML, err := yaml.Marshal(in)
require.NoError(err)
assert.Equal(expectedYAML, string(actualYAML))
var actualJSON2 HexBytes
err = json.Unmarshal(actualJSON, &actualJSON2)
require.NoError(err)
assert.Equal(in, actualJSON2)
var actualYAML2 HexBytes
err = yaml.Unmarshal(actualYAML, &actualYAML2)
require.NoError(err)
assert.Equal(in, actualYAML2)
}