config: add separate option for handling attestation parameters (#1623)

* Add attestation options to config

* Add join-config migration path for clusters with old measurement format

* Always create MAA provider for Azure SNP clusters

* Remove confidential VM option from provider in favor of attestation options

* cli: add config migrate command to handle config migration (#1678)

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2023-05-03 11:11:53 +02:00 committed by GitHub
parent 6027b066e5
commit d7a2ddd939
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1339 additions and 1282 deletions

View File

@ -22,7 +22,6 @@ go_library(
"//cli/internal/terraform",
"//internal/atls",
"//internal/attestation/choose",
"//internal/attestation/idkeydigest",
"//internal/attestation/measurements",
"//internal/cloud/cloudprovider",
"//internal/cloud/gcpshared",
@ -51,20 +50,11 @@ go_test(
deps = [
"//cli/internal/iamid",
"//cli/internal/terraform",
"//internal/atls",
"//internal/attestation/azure/snp",
"//internal/attestation/azure/trustedlaunch",
"//internal/attestation/gcp",
"//internal/attestation/idkeydigest",
"//internal/attestation/measurements",
"//internal/attestation/qemu",
"//internal/cloud/cloudprovider",
"//internal/cloud/gcpshared",
"//internal/config",
"//internal/logger",
"//internal/variant",
"@com_github_hashicorp_terraform_json//:terraform-json",
"@com_github_spf13_cobra//:cobra",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@org_uber_go_goleak//:goleak",

View File

@ -28,7 +28,6 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/image"
"github.com/edgelesssys/constellation/v2/cli/internal/libvirt"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -218,15 +217,11 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts Crea
StateDiskType: opts.Config.Provider.Azure.StateDiskType,
ImageID: opts.image,
SecureBoot: *opts.Config.Provider.Azure.SecureBoot,
CreateMAA: opts.Config.Provider.Azure.EnforceIDKeyDigest == idkeydigest.MAAFallback,
CreateMAA: opts.Config.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{}),
Debug: opts.Config.IsDebugCluster(),
}
attestVariant, err := variant.FromString(opts.Config.AttestationVariant)
if err != nil {
return clusterid.File{}, fmt.Errorf("parsing attestation variant: %w", err)
}
vars.ConfidentialVM = attestVariant.Equal(variant.AzureSEVSNP{})
vars.ConfidentialVM = opts.Config.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})
vars = normalizeAzureURIs(vars)

View File

@ -18,7 +18,6 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/variant"
)
func TestCreator(t *testing.T) {
@ -63,7 +62,7 @@ func TestCreator(t *testing.T) {
provider: cloudprovider.Azure,
config: func() *config.Config {
cfg := config.Default()
cfg.AttestationVariant = variant.AzureSEVSNP{}.String()
cfg.RemoveProviderExcept(cloudprovider.Azure)
return cfg
}(),
policyPatcher: &stubPolicyPatcher{},
@ -73,7 +72,9 @@ func TestCreator(t *testing.T) {
provider: cloudprovider.Azure,
config: func() *config.Config {
cfg := config.Default()
cfg.AttestationVariant = variant.AzureTrustedLaunch{}.String()
cfg.Attestation = config.AttestationConfig{
AzureTrustedLaunch: &config.AzureTrustedLaunch{},
}
return cfg
}(),
policyPatcher: &stubPolicyPatcher{},
@ -83,7 +84,7 @@ func TestCreator(t *testing.T) {
provider: cloudprovider.Azure,
config: func() *config.Config {
cfg := config.Default()
cfg.AttestationVariant = variant.AzureSEVSNP{}.String()
cfg.RemoveProviderExcept(cloudprovider.Azure)
return cfg
}(),
policyPatcher: &stubPolicyPatcher{someErr},
@ -94,7 +95,7 @@ func TestCreator(t *testing.T) {
provider: cloudprovider.Azure,
config: func() *config.Config {
cfg := config.Default()
cfg.AttestationVariant = variant.AzureSEVSNP{}.String()
cfg.RemoveProviderExcept(cloudprovider.Azure)
return cfg
}(),
policyPatcher: &stubPolicyPatcher{},
@ -105,7 +106,7 @@ func TestCreator(t *testing.T) {
provider: cloudprovider.Azure,
config: func() *config.Config {
cfg := config.Default()
cfg.AttestationVariant = variant.AzureSEVSNP{}.String()
cfg.RemoveProviderExcept(cloudprovider.Azure)
return cfg
}(),
policyPatcher: &stubPolicyPatcher{},

View File

@ -10,56 +10,27 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/choose"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/spf13/cobra"
)
// Validator validates Platform Configuration Registers (PCRs).
type Validator struct {
attestationVariant variant.Variant
pcrs measurements.M
idKeyConfig config.SNPFirmwareSignerConfig
validator atls.Validator
log debugLog
}
// NewValidator creates a new Validator.
func NewValidator(conf *config.Config, maaURL string, log debugLog) (*Validator, error) {
v := Validator{log: log}
attestVariant, err := variant.FromString(conf.AttestationVariant)
if err != nil {
return nil, fmt.Errorf("parsing attestation variant: %w", err)
}
v.attestationVariant = attestVariant // valid variant
if err := v.setPCRs(conf); err != nil {
return nil, err
}
if v.attestationVariant.Equal(variant.AzureSEVSNP{}) {
v.idKeyConfig = config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: conf.Provider.Azure.IDKeyDigest,
EnforcementPolicy: conf.IDKeyDigestPolicy(),
MAAURL: maaURL,
}
}
return &v, nil
func NewValidator(cmd *cobra.Command, config config.AttestationCfg, log debugLog) (atls.Validator, error) {
return choose.Validator(config, warnLogger{cmd: cmd, log: log})
}
// UpdateInitPCRs sets the owner and cluster PCR values.
func (v *Validator) UpdateInitPCRs(ownerID, clusterID string) error {
if err := v.updatePCR(uint32(measurements.PCRIndexOwnerID), ownerID); err != nil {
func UpdateInitPCRs(config config.AttestationCfg, ownerID, clusterID string) error {
m := config.GetMeasurements()
if err := updatePCR(m, uint32(measurements.PCRIndexOwnerID), ownerID); err != nil {
return err
}
return v.updatePCR(uint32(measurements.PCRIndexClusterID), clusterID)
return updatePCR(m, uint32(measurements.PCRIndexClusterID), clusterID)
}
// updatePCR adds a new entry to the measurements of v, or removes the key if the input is an empty string.
@ -67,9 +38,9 @@ func (v *Validator) UpdateInitPCRs(ownerID, clusterID string) error {
// When adding, the input is first decoded from hex or base64.
// We then calculate the expected PCR by hashing the input using SHA256,
// appending expected PCR for initialization, and then hashing once more.
func (v *Validator) updatePCR(pcrIndex uint32, encoded string) error {
func updatePCR(m measurements.M, pcrIndex uint32, encoded string) error {
if encoded == "" {
delete(v.pcrs, pcrIndex)
delete(m, pcrIndex)
return nil
}
@ -85,42 +56,15 @@ func (v *Validator) updatePCR(pcrIndex uint32, encoded string) error {
// new_pcr_value := hash(old_pcr_value || data_to_extend)
// Since we use the TPM2_PCR_Event call to extend the PCR, data_to_extend is the hash of our input
hashedInput := sha256.Sum256(decoded)
oldExpected := v.pcrs[pcrIndex].Expected
oldExpected := m[pcrIndex].Expected
expectedPcr := sha256.Sum256(append(oldExpected[:], hashedInput[:]...))
v.pcrs[pcrIndex] = measurements.Measurement{
m[pcrIndex] = measurements.Measurement{
Expected: expectedPcr,
ValidationOpt: v.pcrs[pcrIndex].ValidationOpt,
ValidationOpt: m[pcrIndex].ValidationOpt,
}
return nil
}
func (v *Validator) setPCRs(config *config.Config) error {
measurements := config.GetMeasurements()
if len(measurements) == 0 {
return errors.New("no measurements found in config")
}
v.pcrs = measurements
return nil
}
// V returns the validator as atls.Validator.
func (v *Validator) V(cmd *cobra.Command) atls.Validator {
v.updateValidator(cmd)
return v.validator
}
// PCRS returns the validator's PCR map.
func (v *Validator) PCRS() measurements.M {
return v.pcrs
}
func (v *Validator) updateValidator(cmd *cobra.Command) {
log := warnLogger{cmd: cmd, log: v.log}
// Use of a valid variant has been check in NewValidator so we may drop the error
v.validator, _ = choose.Validator(v.attestationVariant, v.pcrs, v.idKeyConfig, log)
}
// warnLogger implements logging of warnings for validators.
type warnLogger struct {
cmd *cobra.Command

View File

@ -9,212 +9,14 @@ package cloudcmd
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"testing"
"github.com/spf13/cobra"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/v2/internal/attestation/gcp"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/qemu"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/variant"
)
func TestNewValidator(t *testing.T) {
testPCRs := measurements.M{
0: measurements.WithAllBytes(0x00, measurements.Enforce),
1: measurements.WithAllBytes(0xFF, measurements.Enforce),
2: measurements.WithAllBytes(0x00, measurements.Enforce),
3: measurements.WithAllBytes(0xFF, measurements.Enforce),
4: measurements.WithAllBytes(0x00, measurements.Enforce),
5: measurements.WithAllBytes(0x00, measurements.Enforce),
}
testCases := map[string]struct {
config *config.Config
wantErr bool
}{
"gcp": {
config: &config.Config{
AttestationVariant: variant.GCPSEVES{}.String(),
Provider: config.ProviderConfig{
GCP: &config.GCPConfig{
Measurements: testPCRs,
},
},
},
},
"azure cvm": {
config: &config.Config{
AttestationVariant: variant.AzureSEVSNP{}.String(),
Provider: config.ProviderConfig{
Azure: &config.AzureConfig{
Measurements: testPCRs,
},
},
},
},
"azure trusted launch": {
config: &config.Config{
AttestationVariant: variant.AzureTrustedLaunch{}.String(),
Provider: config.ProviderConfig{
Azure: &config.AzureConfig{
Measurements: testPCRs,
},
},
},
},
"openstack": {
config: &config.Config{
AttestationVariant: variant.QEMUVTPM{}.String(),
Provider: config.ProviderConfig{
OpenStack: &config.OpenStackConfig{
Measurements: testPCRs,
},
},
},
},
"qemu": {
config: &config.Config{
AttestationVariant: variant.QEMUVTPM{}.String(),
Provider: config.ProviderConfig{
QEMU: &config.QEMUConfig{
Measurements: testPCRs,
},
},
},
},
"no pcrs provided": {
config: &config.Config{
AttestationVariant: variant.AzureSEVSNP{}.String(),
Provider: config.ProviderConfig{
Azure: &config.AzureConfig{
Measurements: measurements.M{},
},
},
},
wantErr: true,
},
"unknown variant": {
config: &config.Config{
AttestationVariant: "unknown",
Provider: config.ProviderConfig{
QEMU: &config.QEMUConfig{
Measurements: testPCRs,
},
},
},
wantErr: true,
},
"set idkeydigest": {
config: &config.Config{
AttestationVariant: variant.AzureSEVSNP{}.String(),
Provider: config.ProviderConfig{
Azure: &config.AzureConfig{
Measurements: testPCRs,
IDKeyDigest: idkeydigest.List{[]byte("414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141")},
EnforceIDKeyDigest: idkeydigest.Equal,
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
validators, err := NewValidator(tc.config, "https://192.0.2.1:8080/maa", logger.NewTest(t))
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.config.GetMeasurements(), validators.pcrs)
variant, err := variant.FromString(tc.config.AttestationVariant)
require.NoError(t, err)
assert.Equal(variant, validators.attestationVariant)
}
})
}
}
func TestValidatorV(t *testing.T) {
newTestPCRs := func() measurements.M {
return measurements.M{
0: measurements.WithAllBytes(0x00, measurements.WarnOnly),
1: measurements.WithAllBytes(0x00, measurements.WarnOnly),
2: measurements.WithAllBytes(0x00, measurements.WarnOnly),
3: measurements.WithAllBytes(0x00, measurements.WarnOnly),
4: measurements.WithAllBytes(0x00, measurements.WarnOnly),
5: measurements.WithAllBytes(0x00, measurements.WarnOnly),
6: measurements.WithAllBytes(0x00, measurements.WarnOnly),
7: measurements.WithAllBytes(0x00, measurements.WarnOnly),
8: measurements.WithAllBytes(0x00, measurements.WarnOnly),
9: measurements.WithAllBytes(0x00, measurements.WarnOnly),
10: measurements.WithAllBytes(0x00, measurements.WarnOnly),
11: measurements.WithAllBytes(0x00, measurements.WarnOnly),
12: measurements.WithAllBytes(0x00, measurements.WarnOnly),
}
}
testCases := map[string]struct {
variant variant.Variant
pcrs measurements.M
wantVs atls.Validator
}{
"gcp": {
variant: variant.GCPSEVES{},
pcrs: newTestPCRs(),
wantVs: gcp.NewValidator(config.GCPSEVES{Measurements: newTestPCRs()}, nil),
},
"azure cvm": {
variant: variant.AzureSEVSNP{},
pcrs: newTestPCRs(),
wantVs: snp.NewValidator(
config.AzureSEVSNP{
Measurements: newTestPCRs(),
FirmwareSignerConfig: config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: idkeydigest.List{},
EnforcementPolicy: idkeydigest.WarnOnly,
},
},
nil,
),
},
"azure trusted launch": {
variant: variant.AzureTrustedLaunch{},
pcrs: newTestPCRs(),
wantVs: trustedlaunch.NewValidator(config.AzureTrustedLaunch{Measurements: newTestPCRs()}, nil),
},
"qemu": {
variant: variant.QEMUVTPM{},
pcrs: newTestPCRs(),
wantVs: qemu.NewValidator(config.QEMUVTPM{Measurements: newTestPCRs()}, nil),
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
validators := &Validator{attestationVariant: tc.variant, pcrs: tc.pcrs}
resultValidator := validators.V(&cobra.Command{})
assert.Equal(tc.wantVs.OID(), resultValidator.OID())
})
}
}
func TestValidatorUpdateInitPCRs(t *testing.T) {
zero := measurements.WithAllBytes(0x00, measurements.WarnOnly)
one := measurements.WithAllBytes(0x11, measurements.WarnOnly)
@ -251,51 +53,58 @@ func TestValidatorUpdateInitPCRs(t *testing.T) {
}
testCases := map[string]struct {
variant variant.Variant
pcrs measurements.M
config config.AttestationCfg
ownerID string
clusterID string
wantErr bool
}{
"gcp update owner ID": {
variant: variant.GCPSEVES{},
pcrs: newTestPCRs(),
config: &config.GCPSEVES{
Measurements: newTestPCRs(),
},
ownerID: one64,
},
"gcp update cluster ID": {
variant: variant.GCPSEVES{},
pcrs: newTestPCRs(),
config: &config.GCPSEVES{
Measurements: newTestPCRs(),
},
clusterID: one64,
},
"gcp update both": {
variant: variant.GCPSEVES{},
pcrs: newTestPCRs(),
config: &config.GCPSEVES{
Measurements: newTestPCRs(),
},
ownerID: one64,
clusterID: one64,
},
"azure update owner ID": {
variant: variant.AzureSEVSNP{},
pcrs: newTestPCRs(),
config: &config.AzureSEVSNP{
Measurements: newTestPCRs(),
},
ownerID: one64,
},
"azure update cluster ID": {
variant: variant.AzureSEVSNP{},
pcrs: newTestPCRs(),
config: &config.AzureSEVSNP{
Measurements: newTestPCRs(),
},
clusterID: one64,
},
"azure update both": {
variant: variant.AzureSEVSNP{},
pcrs: newTestPCRs(),
config: &config.AzureSEVSNP{
Measurements: newTestPCRs(),
},
ownerID: one64,
clusterID: one64,
},
"owner ID and cluster ID empty": {
variant: variant.GCPSEVES{},
pcrs: newTestPCRs(),
config: &config.AzureSEVSNP{
Measurements: newTestPCRs(),
},
},
"invalid encoding": {
variant: variant.GCPSEVES{},
pcrs: newTestPCRs(),
config: &config.GCPSEVES{
Measurements: newTestPCRs(),
},
ownerID: "invalid",
wantErr: true,
},
@ -305,149 +114,44 @@ func TestValidatorUpdateInitPCRs(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
validators := &Validator{attestationVariant: tc.variant, pcrs: tc.pcrs}
err := validators.UpdateInitPCRs(tc.ownerID, tc.clusterID)
err := UpdateInitPCRs(tc.config, tc.ownerID, tc.clusterID)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
for i := 0; i < len(tc.pcrs); i++ {
require.NoError(t, err)
m := tc.config.GetMeasurements()
for i := 0; i < len(m); i++ {
switch {
case i == int(measurements.PCRIndexClusterID) && tc.clusterID == "":
// should be deleted
_, ok := validators.pcrs[uint32(i)]
_, ok := m[uint32(i)]
assert.False(ok)
case i == int(measurements.PCRIndexClusterID):
pcr, ok := validators.pcrs[uint32(i)]
pcr, ok := m[uint32(i)]
assert.True(ok)
assert.Equal(pcrZeroUpdatedOne, pcr.Expected)
case i == int(measurements.PCRIndexOwnerID) && tc.ownerID == "":
// should be deleted
_, ok := validators.pcrs[uint32(i)]
_, ok := m[uint32(i)]
assert.False(ok)
case i == int(measurements.PCRIndexOwnerID):
pcr, ok := validators.pcrs[uint32(i)]
pcr, ok := m[uint32(i)]
assert.True(ok)
assert.Equal(pcrZeroUpdatedOne, pcr.Expected)
default:
if i >= 17 && i <= 22 {
assert.Equal(one, validators.pcrs[uint32(i)])
assert.Equal(one, m[uint32(i)])
} else {
assert.Equal(zero, validators.pcrs[uint32(i)])
assert.Equal(zero, m[uint32(i)])
}
}
}
})
}
}
func TestUpdatePCR(t *testing.T) {
emptyMap := measurements.M{}
defaultMap := measurements.M{
0: measurements.WithAllBytes(0xAA, measurements.Enforce),
1: measurements.WithAllBytes(0xBB, measurements.Enforce),
}
testCases := map[string]struct {
pcrMap measurements.M
pcrIndex uint32
encoded string
wantEntries int
wantErr bool
}{
"empty input, empty map": {
pcrMap: emptyMap,
pcrIndex: 10,
encoded: "",
wantEntries: 0,
wantErr: false,
},
"empty input, default map": {
pcrMap: defaultMap,
pcrIndex: 10,
encoded: "",
wantEntries: len(defaultMap),
wantErr: false,
},
"correct input, empty map": {
pcrMap: emptyMap,
pcrIndex: 10,
encoded: base64.StdEncoding.EncodeToString([]byte("Constellation")),
wantEntries: 1,
wantErr: false,
},
"correct input, default map": {
pcrMap: defaultMap,
pcrIndex: 10,
encoded: base64.StdEncoding.EncodeToString([]byte("Constellation")),
wantEntries: len(defaultMap) + 1,
wantErr: false,
},
"hex input, empty map": {
pcrMap: emptyMap,
pcrIndex: 10,
encoded: hex.EncodeToString([]byte("Constellation")),
wantEntries: 1,
wantErr: false,
},
"hex input, default map": {
pcrMap: defaultMap,
pcrIndex: 10,
encoded: hex.EncodeToString([]byte("Constellation")),
wantEntries: len(defaultMap) + 1,
wantErr: false,
},
"unencoded input, empty map": {
pcrMap: emptyMap,
pcrIndex: 10,
encoded: "Constellation",
wantEntries: 0,
wantErr: true,
},
"unencoded input, default map": {
pcrMap: defaultMap,
pcrIndex: 10,
encoded: "Constellation",
wantEntries: len(defaultMap),
wantErr: true,
},
"empty input at occupied index": {
pcrMap: defaultMap,
pcrIndex: 0,
encoded: "",
wantEntries: len(defaultMap) - 1,
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
pcrs := make(measurements.M)
for k, v := range tc.pcrMap {
pcrs[k] = v
}
validators := &Validator{
attestationVariant: variant.GCPSEVES{},
pcrs: pcrs,
}
err := validators.updatePCR(tc.pcrIndex, tc.encoded)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
assert.Len(pcrs, tc.wantEntries)
})
}
}

View File

@ -11,6 +11,7 @@ go_library(
"configgenerate.go",
"configinstancetypes.go",
"configkubernetesversions.go",
"configmigrate.go",
"create.go",
"iamcreate.go",
"iamdestroy.go",
@ -45,7 +46,6 @@ go_library(
"//cli/internal/terraform",
"//disk-mapper/recoverproto",
"//internal/atls",
"//internal/attestation/idkeydigest",
"//internal/attestation/measurements",
"//internal/cloud/azureshared",
"//internal/cloud/cloudprovider",
@ -54,6 +54,7 @@ go_library(
"//internal/compatibility",
"//internal/config",
"//internal/config/instancetypes",
"//internal/config/migration",
"//internal/constants",
"//internal/crypto",
"//internal/file",

View File

@ -24,6 +24,7 @@ func NewConfigCmd() *cobra.Command {
cmd.AddCommand(newConfigFetchMeasurementsCmd())
cmd.AddCommand(newConfigInstanceTypesCmd())
cmd.AddCommand(newConfigKubernetesVersionsCmd())
cmd.AddCommand(newConfigMigrateCmd())
return cmd
}

View File

@ -0,0 +1,61 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/config/migration"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
func newConfigMigrateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "migrate",
Short: "Migrate a configuration file to a new version",
Long: "Migrate a configuration file to a new version.",
Args: cobra.NoArgs,
RunE: runConfigMigrate,
}
return cmd
}
func runConfigMigrate(cmd *cobra.Command, _ []string) error {
handler := file.NewHandler(afero.NewOsFs())
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("parsing config path flag: %w", err)
}
return configMigrate(cmd, configPath, handler)
}
func configMigrate(cmd *cobra.Command, configPath string, handler file.Handler) error {
// Make sure we are reading a v2 config
var cfgVersion struct {
Version string `yaml:"version"`
}
if err := handler.ReadYAML(configPath, &cfgVersion); err != nil {
return err
}
switch cfgVersion.Version {
case config.Version3:
cmd.Printf("Config already at version %s, nothing to do", config.Version3)
return nil
case migration.Version2:
if err := migration.V2ToV3(configPath, handler); err != nil {
return fmt.Errorf("migrating config: %w", err)
}
cmd.Printf("Successfully migrated config to %s\n", config.Version3)
return nil
default:
return fmt.Errorf("cannot convert config version %s to %s", cfgVersion.Version, config.Version3)
}
}

View File

@ -13,7 +13,6 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -97,17 +96,9 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler
printedAWarning = true
}
attestVariant, err := variant.FromString(conf.AttestationVariant)
if err != nil {
return fmt.Errorf("parsing attestation variant: %w", err)
}
if attestVariant.Equal(variant.AzureTrustedLaunch{}) {
if conf.GetAttestationConfig().GetVariant().Equal(variant.AzureTrustedLaunch{}) {
cmd.PrintErrln("Disabling Confidential VMs is insecure. Use only for evaluation purposes.")
printedAWarning = true
if conf.IDKeyDigestPolicy() == idkeydigest.Equal || conf.IDKeyDigestPolicy() == idkeydigest.MAAFallback {
cmd.PrintErrln("Your config asks for validating the idkeydigest. This is only available on Confidential VMs. It will not be enforced.")
}
}
// Print an extra new line later to separate warnings from the prompt message of the create command

View File

@ -19,6 +19,7 @@ import (
"text/tabwriter"
"time"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/bootstrapper/initproto"
@ -79,8 +80,8 @@ func runInitialize(cmd *cobra.Command, _ []string) error {
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
newDialer := func(validator *cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, validator.V(cmd), &net.Dialer{})
newDialer := func(validator atls.Validator) *dialer.Dialer {
return dialer.New(nil, validator, &net.Dialer{})
}
spinner, err := newSpinnerOrStderr(cmd)
@ -97,7 +98,7 @@ func runInitialize(cmd *cobra.Command, _ []string) error {
}
// initialize initializes a Constellation.
func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator) *dialer.Dialer,
func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator atls.Validator) *dialer.Dialer,
fileHandler file.Handler, quotaChecker license.QuotaChecker,
) error {
flags, err := i.evalFlagArgs(cmd)
@ -138,8 +139,9 @@ func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator *cloud
}
i.log.Debugf("Checked license")
i.log.Debugf("Creating aTLS Validator for %s", conf.AttestationVariant)
validator, err := cloudcmd.NewValidator(conf, idFile.AttestationURL, i.log)
conf.UpdateMAAURL(idFile.AttestationURL)
i.log.Debugf("Creating aTLS Validator for %s", conf.GetAttestationConfig().GetVariant())
validator, err := cloudcmd.NewValidator(cmd, conf.GetAttestationConfig(), i.log)
if err != nil {
return err
}
@ -155,7 +157,7 @@ func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator *cloud
}
helmLoader := helm.NewLoader(provider, k8sVersion)
i.log.Debugf("Created new Helm loader")
helmDeployments, err := helmLoader.Load(conf, flags.conformance, masterSecret.Key, masterSecret.Salt, idFile.AttestationURL)
helmDeployments, err := helmLoader.Load(conf, flags.conformance, masterSecret.Key, masterSecret.Salt)
i.log.Debugf("Loaded Helm deployments")
if err != nil {
return fmt.Errorf("loading Helm charts: %w", err)

View File

@ -19,8 +19,8 @@ import (
"time"
"github.com/edgelesssys/constellation/v2/bootstrapper/initproto"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
@ -131,7 +131,7 @@ func TestInitialize(t *testing.T) {
// Networking
netDialer := testdialer.NewBufconnDialer()
newDialer := func(*cloudcmd.Validator) *dialer.Dialer {
newDialer := func(atls.Validator) *dialer.Dialer {
return dialer.New(nil, nil, netDialer)
}
serverCreds := atlscredentials.New(nil, nil)
@ -397,13 +397,6 @@ func TestAttestation(t *testing.T) {
existingIDFile := &clusterid.File{IP: "192.0.2.4", CloudProvider: cloudprovider.QEMU}
netDialer := testdialer.NewBufconnDialer()
newDialer := func(v *cloudcmd.Validator) *dialer.Dialer {
validator := &testValidator{
Getter: variant.QEMUVTPM{},
pcrs: v.PCRS(),
}
return dialer.New(nil, validator, netDialer)
}
issuer := &testIssuer{
Getter: variant.QEMUVTPM{},
@ -436,17 +429,24 @@ func TestAttestation(t *testing.T) {
cfg := config.Default()
cfg.Image = "image"
cfg.AttestationVariant = variant.QEMUVTPM{}.String()
cfg.RemoveProviderExcept(cloudprovider.QEMU)
cfg.Provider.QEMU.Measurements[0] = measurements.WithAllBytes(0x00, measurements.Enforce)
cfg.Provider.QEMU.Measurements[1] = measurements.WithAllBytes(0x11, measurements.Enforce)
cfg.Provider.QEMU.Measurements[2] = measurements.WithAllBytes(0x22, measurements.Enforce)
cfg.Provider.QEMU.Measurements[3] = measurements.WithAllBytes(0x33, measurements.Enforce)
cfg.Provider.QEMU.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
cfg.Provider.QEMU.Measurements[9] = measurements.WithAllBytes(0x99, measurements.Enforce)
cfg.Provider.QEMU.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
cfg.Attestation.QEMUVTPM.Measurements[0] = measurements.WithAllBytes(0x00, measurements.Enforce)
cfg.Attestation.QEMUVTPM.Measurements[1] = measurements.WithAllBytes(0x11, measurements.Enforce)
cfg.Attestation.QEMUVTPM.Measurements[2] = measurements.WithAllBytes(0x22, measurements.Enforce)
cfg.Attestation.QEMUVTPM.Measurements[3] = measurements.WithAllBytes(0x33, measurements.Enforce)
cfg.Attestation.QEMUVTPM.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
cfg.Attestation.QEMUVTPM.Measurements[9] = measurements.WithAllBytes(0x99, measurements.Enforce)
cfg.Attestation.QEMUVTPM.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, cfg, file.OptNone))
newDialer := func(v atls.Validator) *dialer.Dialer {
validator := &testValidator{
Getter: variant.QEMUVTPM{},
pcrs: cfg.GetAttestationConfig().GetMeasurements(),
}
return dialer.New(nil, validator, netDialer)
}
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
@ -530,7 +530,6 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs
switch csp {
case cloudprovider.Azure:
conf.AttestationVariant = variant.AzureSEVSNP{}.String()
conf.Provider.Azure.SubscriptionID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.TenantID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.Location = "test-location"
@ -538,23 +537,21 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs
conf.Provider.Azure.ResourceGroup = "test-resource-group"
conf.Provider.Azure.AppClientID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.ClientSecretValue = "test-client-secret"
conf.Provider.Azure.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
conf.Provider.Azure.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce)
conf.Provider.Azure.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
conf.Attestation.AzureSEVSNP.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
conf.Attestation.AzureSEVSNP.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce)
conf.Attestation.AzureSEVSNP.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
case cloudprovider.GCP:
conf.AttestationVariant = variant.GCPSEVES{}.String()
conf.Provider.GCP.Region = "test-region"
conf.Provider.GCP.Project = "test-project"
conf.Provider.GCP.Zone = "test-zone"
conf.Provider.GCP.ServiceAccountKeyPath = "test-key-path"
conf.Provider.GCP.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
conf.Provider.GCP.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce)
conf.Provider.GCP.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
conf.Attestation.GCPSEVES.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
conf.Attestation.GCPSEVES.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce)
conf.Attestation.GCPSEVES.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
case cloudprovider.QEMU:
conf.AttestationVariant = variant.QEMUVTPM{}.String()
conf.Provider.QEMU.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
conf.Provider.QEMU.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce)
conf.Provider.QEMU.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
conf.Attestation.QEMUVTPM.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce)
conf.Attestation.QEMUVTPM.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce)
conf.Attestation.QEMUVTPM.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce)
}
conf.RemoveProviderExcept(csp)

View File

@ -20,6 +20,7 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/libvirt"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -255,8 +256,8 @@ func (m *miniUpCmd) initializeMiniCluster(cmd *cobra.Command, fileHandler file.H
cmd.PrintErrf("Rollback succeeded.\n\n")
}
}()
newDialer := func(validator *cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, validator.V(cmd), &net.Dialer{})
newDialer := func(validator atls.Validator) *dialer.Dialer {
return dialer.New(nil, validator, &net.Dialer{})
}
m.log.Debugf("Created new dialer")
cmd.Flags().String("master-secret", "", "")

View File

@ -18,6 +18,7 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/disk-mapper/recoverproto"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -57,8 +58,8 @@ func runRecover(cmd *cobra.Command, _ []string) error {
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
newDialer := func(validator *cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, validator.V(cmd), &net.Dialer{})
newDialer := func(validator atls.Validator) *dialer.Dialer {
return dialer.New(nil, validator, &net.Dialer{})
}
r := &recoverCmd{log: log}
return r.recover(cmd, fileHandler, 5*time.Second, &recoverDoer{log: r.log}, newDialer)
@ -66,7 +67,7 @@ func runRecover(cmd *cobra.Command, _ []string) error {
func (r *recoverCmd) recover(
cmd *cobra.Command, fileHandler file.Handler, interval time.Duration,
doer recoverDoerInterface, newDialer func(validator *cloudcmd.Validator) *dialer.Dialer,
doer recoverDoerInterface, newDialer func(validator atls.Validator) *dialer.Dialer,
) error {
flags, err := r.parseRecoverFlags(cmd, fileHandler)
if err != nil {
@ -96,8 +97,9 @@ func (r *recoverCmd) recover(
interval = 20 * time.Second // Azure LB takes a while to remove unhealthy instances
}
r.log.Debugf("Creating aTLS Validator for %s", conf.AttestationVariant)
validator, err := cloudcmd.NewValidator(conf, flags.maaURL, r.log)
conf.UpdateMAAURL(flags.maaURL)
r.log.Debugf("Creating aTLS Validator for %s", conf.GetAttestationConfig().GetVariant())
validator, err := cloudcmd.NewValidator(cmd, conf.GetAttestationConfig(), r.log)
if err != nil {
return err
}

View File

@ -15,9 +15,9 @@ import (
"testing"
"time"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/disk-mapper/recoverproto"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -162,7 +162,7 @@ func TestRecover(t *testing.T) {
file.OptNone,
))
newDialer := func(*cloudcmd.Validator) *dialer.Dialer { return nil }
newDialer := func(atls.Validator) *dialer.Dialer { return nil }
r := &recoverCmd{log: logger.NewTest(t)}
err := r.recover(cmd, fileHandler, time.Millisecond, tc.doer, newDialer)
if tc.wantErr {

View File

@ -12,13 +12,15 @@ import (
"fmt"
"time"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
"github.com/edgelesssys/constellation/v2/cli/internal/kubernetes"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/spf13/afero"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
@ -81,6 +83,17 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand
return err
}
var idFile clusterid.File
if err := fileHandler.ReadJSON(constants.ClusterIDsFileName, &idFile); err != nil {
return fmt.Errorf("reading cluster ID file: %w", err)
}
conf.UpdateMAAURL(idFile.AttestationURL)
// If an image upgrade was just executed there won't be a diff. The function will return nil in that case.
if err := u.upgradeAttestConfigIfDiff(cmd, conf.GetAttestationConfig(), flags); err != nil {
return fmt.Errorf("upgrading measurements: %w", err)
}
if conf.GetProvider() == cloudprovider.Azure || conf.GetProvider() == cloudprovider.GCP {
err = u.handleServiceUpgrade(cmd, conf, flags)
upgradeErr := &compatibility.InvalidUpgradeError{}
@ -104,37 +117,37 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand
cmd.PrintErrln("WARNING: Skipping service and image upgrades, which are currently only supported for Azure and GCP.")
}
// If an image upgrade was just executed there won't be a diff. The function will return nil in that case.
if err := u.upgradeMeasurementsIfDiff(cmd, conf.GetMeasurements(), flags); err != nil {
return fmt.Errorf("upgrading measurements: %w", err)
}
return nil
}
// upgradeMeasurementsIfDiff checks if the locally configured measurements are different from the cluster's measurements.
// upgradeAttestConfigIfDiff checks if the locally configured measurements are different from the cluster's measurements.
// If so the function will ask the user to confirm (if --yes is not set) and upgrade the measurements only.
func (u *upgradeApplyCmd) upgradeMeasurementsIfDiff(cmd *cobra.Command, newMeasurements measurements.M, flags upgradeApplyFlags) error {
clusterMeasurements, _, err := u.upgrader.GetClusterMeasurements(cmd.Context())
if err != nil {
func (u *upgradeApplyCmd) upgradeAttestConfigIfDiff(cmd *cobra.Command, newConfig config.AttestationCfg, flags upgradeApplyFlags) error {
clusterAttestationConfig, _, err := u.upgrader.GetClusterAttestationConfig(cmd.Context(), newConfig.GetVariant())
// Config migration from v2.7 to v2.8 requires us to skip comparing configs if the cluster is still using the legacy config.
// TODO: v2.9 Remove error type check and always run comparison.
if err != nil && !errors.Is(err, kubernetes.ErrLegacyJoinConfig) {
return fmt.Errorf("getting cluster measurements: %w", err)
}
if clusterMeasurements.EqualTo(newMeasurements) {
return nil
if err == nil {
// If the current config is equal, or there is an error when comparing the configs, we skip the upgrade.
if equal, err := newConfig.EqualTo(clusterAttestationConfig); err != nil || equal {
return err
}
}
if !flags.yes {
ok, err := askToConfirm(cmd, "You are about to change your cluster's measurements. Are you sure you want to continue?")
ok, err := askToConfirm(cmd, "You are about to change your cluster's attestation config. Are you sure you want to continue?")
if err != nil {
return fmt.Errorf("asking for confirmation: %w", err)
}
if !ok {
cmd.Println("Aborting upgrade.")
cmd.Println("Skipping upgrade.")
return nil
}
}
if err := u.upgrader.UpdateMeasurements(cmd.Context(), newMeasurements); err != nil {
return fmt.Errorf("updating measurements: %w", err)
if err := u.upgrader.UpdateAttestationConfig(cmd.Context(), newConfig); err != nil {
return fmt.Errorf("updating attestation config: %w", err)
}
return nil
}
@ -149,7 +162,7 @@ func (u *upgradeApplyCmd) handleServiceUpgrade(cmd *cobra.Command, conf *config.
return fmt.Errorf("asking for confirmation: %w", err)
}
if !ok {
cmd.Println("Aborting upgrade.")
cmd.Println("Skipping upgrade.")
return nil
}
}
@ -193,6 +206,6 @@ type upgradeApplyFlags struct {
type cloudUpgrader interface {
UpgradeNodeVersion(ctx context.Context, conf *config.Config) error
UpgradeHelmServices(ctx context.Context, config *config.Config, timeout time.Duration, allowDestructive bool) error
UpdateMeasurements(ctx context.Context, newMeasurements measurements.M) error
GetClusterMeasurements(ctx context.Context) (measurements.M, *corev1.ConfigMap, error)
UpdateAttestationConfig(ctx context.Context, newConfig config.AttestationCfg) error
GetClusterAttestationConfig(ctx context.Context, variant variant.Variant) (config.AttestationCfg, *corev1.ConfigMap, error)
}

View File

@ -12,13 +12,14 @@ import (
"testing"
"time"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/cli/internal/kubernetes"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -32,17 +33,26 @@ func TestUpgradeApply(t *testing.T) {
wantErr bool
}{
"success": {
upgrader: stubUpgrader{},
upgrader: stubUpgrader{currentConfig: config.DefaultForAzureSEVSNP()},
},
"nodeVersion some error": {
upgrader: stubUpgrader{nodeVersionErr: someErr},
upgrader: stubUpgrader{
currentConfig: config.DefaultForAzureSEVSNP(),
nodeVersionErr: someErr,
},
wantErr: true,
},
"nodeVersion in progress error": {
upgrader: stubUpgrader{nodeVersionErr: kubernetes.ErrInProgress},
upgrader: stubUpgrader{
currentConfig: config.DefaultForAzureSEVSNP(),
nodeVersionErr: kubernetes.ErrInProgress,
},
},
"helm other error": {
upgrader: stubUpgrader{helmErr: someErr},
upgrader: stubUpgrader{
currentConfig: config.DefaultForAzureSEVSNP(),
helmErr: someErr,
},
wantErr: true,
},
}
@ -61,6 +71,7 @@ func TestUpgradeApply(t *testing.T) {
handler := file.NewHandler(afero.NewMemMapFs())
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.Azure)
require.NoError(handler.WriteYAML(constants.ConfigFilename, cfg))
require.NoError(handler.WriteJSON(constants.ClusterIDsFileName, clusterid.File{}))
upgrader := upgradeApplyCmd{upgrader: tc.upgrader, log: logger.NewTest(t)}
err = upgrader.upgradeApply(cmd, handler)
@ -74,6 +85,7 @@ func TestUpgradeApply(t *testing.T) {
}
type stubUpgrader struct {
currentConfig config.AttestationCfg
nodeVersionErr error
helmErr error
}
@ -86,10 +98,10 @@ func (u stubUpgrader) UpgradeHelmServices(_ context.Context, _ *config.Config, _
return u.helmErr
}
func (u stubUpgrader) UpdateMeasurements(_ context.Context, _ measurements.M) error {
func (u stubUpgrader) UpdateAttestationConfig(_ context.Context, _ config.AttestationCfg) error {
return nil
}
func (u stubUpgrader) GetClusterMeasurements(_ context.Context) (measurements.M, *corev1.ConfigMap, error) {
return measurements.M{}, &corev1.ConfigMap{}, nil
func (u stubUpgrader) GetClusterAttestationConfig(_ context.Context, _ variant.Variant) (config.AttestationCfg, *corev1.ConfigMap, error) {
return u.currentConfig, &corev1.ConfigMap{}, nil
}

View File

@ -91,15 +91,17 @@ func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC
return fmt.Errorf("loading config file: %w", err)
}
c.log.Debugf("Creating aTLS Validator for %s", conf.AttestationVariant)
validators, err := cloudcmd.NewValidator(conf, flags.maaURL, c.log)
if err != nil {
return fmt.Errorf("creating aTLS validator: %w", err)
conf.UpdateMAAURL(flags.maaURL)
c.log.Debugf("Updating expected PCRs")
attConfig := conf.GetAttestationConfig()
if err := cloudcmd.UpdateInitPCRs(attConfig, flags.ownerID, flags.clusterID); err != nil {
return fmt.Errorf("updating expected PCRs: %w", err)
}
c.log.Debugf("Updating expected PCRs")
if err := validators.UpdateInitPCRs(flags.ownerID, flags.clusterID); err != nil {
return fmt.Errorf("updating expected PCRs: %w", err)
c.log.Debugf("Creating aTLS Validator for %s", conf.GetAttestationConfig().GetVariant())
validator, err := cloudcmd.NewValidator(cmd, attConfig, c.log)
if err != nil {
return fmt.Errorf("creating aTLS validator: %w", err)
}
nonce, err := crypto.GenerateRandomBytes(32)
@ -114,14 +116,14 @@ func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC
&verifyproto.GetAttestationRequest{
Nonce: nonce,
},
validators.V(cmd),
validator,
)
if err != nil {
return fmt.Errorf("verifying: %w", err)
}
// certificates are only available for Azure
attDocOutput, err := formatter.format(rawAttestationDoc, conf.Provider.Azure == nil, flags.rawOutput, validators.PCRS())
attDocOutput, err := formatter.format(rawAttestationDoc, conf.Provider.Azure == nil, flags.rawOutput, attConfig.GetMeasurements())
if err != nil {
return fmt.Errorf("printing attestation document: %w", err)
}

View File

@ -318,9 +318,7 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/helm",
visibility = ["//cli:__subpackages__"],
deps = [
"//cli/internal/clusterid",
"//cli/internal/helm/imageversion",
"//internal/attestation/idkeydigest",
"//internal/cloud/cloudprovider",
"//internal/compatibility",
"//internal/config",
@ -363,7 +361,6 @@ go_test(
"//internal/deploy/helm",
"//internal/file",
"//internal/logger",
"//internal/variant",
"@com_github_pkg_errors//:errors",
"@com_github_spf13_afero//:afero",
"@com_github_stretchr_testify//assert",

View File

@ -5,9 +5,6 @@ metadata:
namespace: {{ .Release.Namespace }}
data:
{{/* mustToJson is required so the json-strings passed from go are of type string in the rendered yaml. */}}
measurements: {{ .Values.measurements | mustToJson }}
{{- if eq .Values.csp "Azure" }}
idKeyConfig: {{ .Values.idKeyConfig | mustToJson }}
{{- end }}
attestationConfig: {{ .Values.attestationConfig | mustToJson }}
binaryData:
measurementSalt: {{ .Values.measurementSalt }}

View File

@ -5,15 +5,10 @@
"description": "CSP to which the chart is deployed.",
"enum": ["AWS", "Azure", "GCP", "OpenStack", "QEMU"]
},
"measurements": {
"description": "JSON-string to describe the expected measurements.",
"attestationConfig": {
"description": "JSON-string to describe the config to use for attestation validation.",
"type": "string",
"examples": ["{'1':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','warnOnly':true},'15':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=','warnOnly':true}}"]
},
"idKeyConfig": {
"description": "Configuration for validating the ID Key Digest of the SEV-SNP attestation.",
"type": "string",
"examples": ["{'enforcementPolicy': 'MAAFallback', 'maaURL': 'https://192.0.2.1:8080/maa', 'acceptedKeyDigests': ['57486a447ec0f1958002a22a06b7673b9fd27d11e1c6527498056054c5fa92d23c50f9de44072760fe2b6fb89740b696', '0356215882a825279a85b300b0b742931d113bf7e32dde2e50ffde7ec743ca491ecdd7f336dc28a6e0b2bb57af7a44a3'}"]
"examples": ["{'measurements':{'1':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','warnOnly':true},'15':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=','warnOnly':true}}}"]
},
"image": {
"description": "Container image to use for the spawned pods.",
@ -33,16 +28,11 @@
},
"required": [
"csp",
"measurements",
"attestationConfig",
"measurementSalt",
"image",
"attestationVariant"
],
"if": {
"properties": { "csp": { "const": "azure" } },
"required": ["csp"]
},
"then": { "required": ["idKeyConfig"] },
"title": "Values",
"type": "object"
}

View File

@ -1,7 +1,5 @@
csp: "gcp"
attestationVariant: ""
measurements: ""
idKeyConfig: ""
measurementSalt: ""
joinServicePort: 9090
joinServiceNodePort: 30090

View File

@ -47,11 +47,6 @@ spec:
- name: config
projected:
sources:
- configMap:
items:
- key: {{ .Values.measurementsFilename | quote }}
path: {{ .Values.measurementsFilename | quote }}
name: {{ .Values.global.joinConfigCMName | quote }}
- secret:
items:
- key: {{ .Values.masterSecretKeyName | quote }}

View File

@ -4,5 +4,3 @@ saltKeyName: salt
masterSecretName: constellation-mastersecret
# Name of the key within the respective secret that holds the master secret.
masterSecretKeyName: mastersecret
# Name of the ConfigMap that holds the measurements.
measurementsFilename: measurements

View File

@ -14,8 +14,7 @@ import (
"strings"
"time"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -147,9 +146,8 @@ func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout tim
return fmt.Errorf("creating CR backup: %w", err)
}
fileHandler := file.NewHandler(afero.NewOsFs())
for _, chart := range upgradeReleases {
err = c.upgradeRelease(ctx, timeout, config, chart, allowDestructive, fileHandler)
err = c.upgradeRelease(ctx, timeout, config, chart, allowDestructive)
if err != nil {
return fmt.Errorf("upgrading %s: %w", chart.Metadata.Name, err)
}
@ -245,7 +243,7 @@ func (s ServiceVersions) ConstellationServices() string {
}
func (c *Client) upgradeRelease(
ctx context.Context, timeout time.Duration, conf *config.Config, chart *chart.Chart, allowDestructive bool, fileHandler file.Handler,
ctx context.Context, timeout time.Duration, conf *config.Config, chart *chart.Chart, allowDestructive bool,
) error {
// We need to load all values that can be statically loaded before merging them with the cluster
// values. Otherwise the templates are not rendered correctly.
@ -289,7 +287,7 @@ func (c *Client) upgradeRelease(
return fmt.Errorf("loading values: %w", err)
}
if err := c.applyMigrations(releaseName, values, conf, fileHandler); err != nil {
if err := c.applyMigrations(ctx, releaseName, values, conf); err != nil {
return fmt.Errorf("applying migrations: %w", err)
}
default:
@ -312,7 +310,7 @@ func (c *Client) upgradeRelease(
// applyMigrations checks the from version and applies the necessary migrations.
// The function assumes the caller has verified that our version drift restriction is not violated,
// Currently, this is done during config validation.
func (c *Client) applyMigrations(releaseName string, values map[string]any, conf *config.Config, fileHandler file.Handler) error {
func (c *Client) applyMigrations(ctx context.Context, releaseName string, values map[string]any, conf *config.Config) error {
current, err := c.currentVersion(releaseName)
if err != nil {
return fmt.Errorf("getting %s version: %w", releaseName, err)
@ -322,37 +320,50 @@ func (c *Client) applyMigrations(releaseName string, values map[string]any, conf
return fmt.Errorf("parsing current version: %w", err)
}
if currentV.Major == 2 && currentV.Minor == 6 {
return migrateFrom2_6(values, conf, fileHandler)
if currentV.Major == 2 && currentV.Minor == 7 {
return migrateFrom2_7(ctx, values, conf, c.kubectl)
}
return nil
}
// migrateFrom2_6 applies the necessary migrations for upgrading from v2.6.x to v2.7.x.
// migrateFrom2_6 should be applied for v2.6.x --> v2.7.x.
// migrateFrom2_6 should NOT be applied for v2.7.0 --> v2.7.x.
// This function can be removed once we are sure that we will no longer provide backports for v2.6.
func migrateFrom2_6(values map[string]any, conf *config.Config, fileHandler file.Handler) error {
// Manually setting attestationVariant is required here since upgrade normally isn't allowed to change this value.
// However, to introduce the value into a 2.6 cluster for the first time we have to set it nevertheless.
if err := setAttestationVariant(values, conf.AttestationVariant); err != nil {
return fmt.Errorf("setting attestationVariant: %w", err)
// migrateFrom2_7 applies the necessary migrations for upgrading from v2.7.x to v2.8.x.
// migrateFrom2_7 should be applied for v2.7.x --> v2.8.x.
// migrateFrom2_7 should NOT be applied for v2.8.0 --> v2.8.x.
// Remove after release of v2.8.0.
func migrateFrom2_7(ctx context.Context, values map[string]any, conf *config.Config, kubeclient crdClient) error {
if conf.GetProvider() == cloudprovider.GCP {
if err := updateGCPStorageClass(ctx, kubeclient); err != nil {
return fmt.Errorf("applying migration for GCP storage class: %w", err)
}
}
// Manually setting idKeyConfig is required here since upgrade normally isn't allowed to change this value.
// However, to introduce the value into a 2.6 cluster for the first time we have to set it nevertheless.
var idFile clusterid.File
if err := fileHandler.ReadJSON(constants.ClusterIDsFileName, &idFile); err != nil {
return fmt.Errorf("reading cluster ID file: %w", err)
return updateJoinConfig(values, conf)
}
func updateGCPStorageClass(ctx context.Context, kubeclient crdClient) error {
// v2.8 updates the disk type of GCP default storage class
// This value is not updatable in Kubernetes, but we can use a workaround to update it:
// First, we delete the storage class, then we upgrade the chart,
// which will recreate the storage class with the new disk type.
if err := kubeclient.DeleteStorageClass(ctx, "encrypted-rwo"); err != nil {
return fmt.Errorf("deleting storage class for update: %w", err)
}
// Disallow users to set MAAFallback as ID key digest policy for upgrades, since it requires extra cloud resources.
if conf.IDKeyDigestPolicy() == idkeydigest.MAAFallback {
return fmt.Errorf("ID key digest policy %s is not supported for upgrades", conf.IDKeyDigestPolicy())
return nil
}
func updateJoinConfig(values map[string]any, conf *config.Config) error {
joinServiceVals, ok := values["join-service"].(map[string]interface{})
if !ok {
return errors.New("invalid join-service config")
}
if err := setIdkeyConfig(values, conf, idFile.AttestationURL); err != nil {
return fmt.Errorf("setting id key config: %w", err)
attestationConfigJSON, err := json.Marshal(conf.GetAttestationConfig())
if err != nil {
return fmt.Errorf("marshalling attestation config: %w", err)
}
joinServiceVals["attestationConfig"] = string(attestationConfigJSON)
return nil
}
@ -404,44 +415,6 @@ func (c *Client) updateCRDs(ctx context.Context, chart *chart.Chart) error {
return nil
}
// setAttestationVariant sets the attesationVariant value on verification-service and join-service value maps.
func setAttestationVariant(values map[string]any, variant string) error {
joinServiceVals, ok := values["join-service"].(map[string]any)
if !ok {
return errors.New("invalid join-service values")
}
joinServiceVals["attestationVariant"] = variant
verifyServiceVals, ok := values["verification-service"].(map[string]any)
if !ok {
return errors.New("invalid verification-service values")
}
verifyServiceVals["attestationVariant"] = variant
return nil
}
// setIdkeyConfig sets the idkeyconfig value on the join-service value maps.
func setIdkeyConfig(values map[string]any, cfg *config.Config, maaURL string) error {
joinServiceVals, ok := values["join-service"].(map[string]any)
if !ok {
return errors.New("invalid join-service values")
}
idKeyCfg := config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: cfg.IDKeyDigests(),
EnforcementPolicy: cfg.IDKeyDigestPolicy(),
MAAURL: maaURL,
}
marshalledCfg, err := json.Marshal(idKeyCfg)
if err != nil {
return fmt.Errorf("marshalling id key digest config: %w", err)
}
joinServiceVals["idKeyConfig"] = string(marshalledCfg)
return nil
}
type debugLog interface {
Debugf(format string, args ...any)
Sync()
@ -452,6 +425,7 @@ type crdClient interface {
ApplyCRD(ctx context.Context, rawCRD []byte) error
GetCRDs(ctx context.Context) ([]apiextensionsv1.CustomResourceDefinition, error)
GetCRs(ctx context.Context, gvr schema.GroupVersionResource) ([]unstructured.Unstructured, error)
DeleteStorageClass(ctx context.Context, name string) error // TODO: remove with v2.9
}
type actionWrapper interface {

View File

@ -13,9 +13,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v3/pkg/chart"
@ -90,8 +88,7 @@ func TestUpgradeRelease(t *testing.T) {
chart, err := loadChartsDir(helmFS, certManagerInfo.path)
require.NoError(err)
fileHandler := file.NewHandler(afero.NewMemMapFs())
err = client.upgradeRelease(context.Background(), 0, config.Default(), chart, tc.allowDestructive, fileHandler)
err = client.upgradeRelease(context.Background(), 0, config.Default(), chart, tc.allowDestructive)
if tc.wantError {
tc.assertCorrectError(t, err)
return

View File

@ -110,7 +110,7 @@ func AvailableServiceVersions() (string, error) {
}
// Load the embedded helm charts.
func (i *ChartLoader) Load(config *config.Config, conformanceMode bool, masterSecret, salt []byte, maaURL string) ([]byte, error) {
func (i *ChartLoader) Load(config *config.Config, conformanceMode bool, masterSecret, salt []byte) ([]byte, error) {
ciliumRelease, err := i.loadRelease(ciliumInfo)
if err != nil {
return nil, fmt.Errorf("loading cilium: %w", err)
@ -131,7 +131,7 @@ func (i *ChartLoader) Load(config *config.Config, conformanceMode bool, masterSe
if err != nil {
return nil, fmt.Errorf("loading constellation-services: %w", err)
}
if err := extendConstellationServicesValues(conServicesRelease.Values, config, masterSecret, salt, maaURL); err != nil {
if err := extendConstellationServicesValues(conServicesRelease.Values, config, masterSecret, salt); err != nil {
return nil, fmt.Errorf("extending constellation-services values: %w", err)
}
@ -378,7 +378,6 @@ func (i *ChartLoader) loadConstellationServicesValues() (map[string]any, error)
"saltKeyName": constants.ConstellationSaltKey,
"masterSecretKeyName": constants.ConstellationMasterSecretKey,
"masterSecretName": constants.ConstellationMasterSecretStoreName,
"measurementsFilename": constants.MeasurementsFilename,
},
"join-service": map[string]any{
"csp": i.csp.String(),
@ -468,7 +467,7 @@ func (i *ChartLoader) loadConstellationServicesValues() (map[string]any, error)
// extendConstellationServicesValues extends the given values map by some values depending on user input.
// Values set inside this function are only applied during init, not during upgrade.
func extendConstellationServicesValues(
in map[string]any, cfg *config.Config, masterSecret, salt []byte, maaURL string,
in map[string]any, cfg *config.Config, masterSecret, salt []byte,
) error {
keyServiceValues, ok := in["key-service"].(map[string]any)
if !ok {
@ -481,41 +480,25 @@ func extendConstellationServicesValues(
if !ok {
return errors.New("invalid join-service values")
}
joinServiceVals["attestationVariant"] = cfg.AttestationVariant
joinServiceVals["attestationVariant"] = cfg.GetAttestationConfig().GetVariant().String()
// measurements are updated separately during upgrade,
// attestation config is updated separately during upgrade,
// so we only set them in Helm during init.
measurementsJSON, err := json.Marshal(cfg.GetMeasurements())
attestationConfigJSON, err := json.Marshal(cfg.GetAttestationConfig())
if err != nil {
return fmt.Errorf("marshalling measurements: %w", err)
}
joinServiceVals["measurements"] = string(measurementsJSON)
joinServiceVals["attestationConfig"] = string(attestationConfigJSON)
verifyServiceVals, ok := in["verification-service"].(map[string]any)
if !ok {
return errors.New("invalid verification-service values")
}
verifyServiceVals["attestationVariant"] = cfg.AttestationVariant
verifyServiceVals["attestationVariant"] = cfg.GetAttestationConfig().GetVariant().String()
csp := cfg.GetProvider()
switch csp {
case cloudprovider.Azure:
joinServiceVals, ok := in["join-service"].(map[string]any)
if !ok {
return errors.New("invalid join-service values")
}
idKeyCfg := config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: cfg.IDKeyDigests(),
EnforcementPolicy: cfg.IDKeyDigestPolicy(),
MAAURL: maaURL,
}
marshalledCfg, err := json.Marshal(idKeyCfg)
if err != nil {
return fmt.Errorf("marshalling id key digest config: %w", err)
}
joinServiceVals["idKeyConfig"] = string(marshalledCfg)
in["azure"] = map[string]any{
"deployCSIDriver": cfg.DeployCSIDriver(),
}

View File

@ -30,7 +30,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
"github.com/edgelesssys/constellation/v2/internal/variant"
)
// TestLoad checks if the serialized format that Load returns correctly preserves the dependencies of the loaded chart.
@ -40,7 +39,7 @@ func TestLoad(t *testing.T) {
config := &config.Config{Provider: config.ProviderConfig{GCP: &config.GCPConfig{}}}
chartLoader := ChartLoader{csp: config.GetProvider()}
release, err := chartLoader.Load(config, true, []byte("secret"), []byte("salt"), "https://192.0.2.1:8080/maa")
release, err := chartLoader.Load(config, true, []byte("secret"), []byte("salt"))
require.NoError(err)
var helmReleases helm.Releases
@ -63,21 +62,25 @@ func TestConstellationServices(t *testing.T) {
}{
"AWS": {
config: &config.Config{
AttestationVariant: variant.AWSNitroTPM{}.String(),
Provider: config.ProviderConfig{AWS: &config.AWSConfig{}},
Attestation: config.AttestationConfig{AWSNitroTPM: &config.AWSNitroTPM{
Measurements: measurements.M{1: measurements.WithAllBytes(0xAA, measurements.Enforce)},
}},
},
valuesModifier: prepareAWSValues,
ccmImage: "ccmImageForAWS",
},
"Azure": {
config: &config.Config{
AttestationVariant: variant.AzureSEVSNP{}.String(),
Provider: config.ProviderConfig{Azure: &config.AzureConfig{
DeployCSIDriver: toPtr(true),
EnforceIDKeyDigest: idkeydigest.Equal,
IDKeyDigest: [][]byte{
{0xba, 0xaa, 0xaa, 0xad, 0xba, 0xaa, 0xaa, 0xad, 0xba, 0xaa, 0xaa, 0xad, 0xba, 0xaa, 0xaa, 0xad, 0xba, 0xaa, 0xaa, 0xad, 0xba, 0xaa, 0xaa, 0xad, 0xba, 0xaa, 0xaa, 0xad, 0xba, 0xaa, 0xaa, 0xad},
{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa},
}},
Attestation: config.AttestationConfig{AzureSEVSNP: &config.AzureSEVSNP{
Measurements: measurements.M{1: measurements.WithAllBytes(0xAA, measurements.Enforce)},
FirmwareSignerConfig: config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: idkeydigest.List{bytes.Repeat([]byte{0xAA}, 32)},
EnforcementPolicy: idkeydigest.MAAFallback,
MAAURL: "https://192.0.2.1:8080/maa",
},
}},
},
@ -88,26 +91,32 @@ func TestConstellationServices(t *testing.T) {
},
"GCP": {
config: &config.Config{
AttestationVariant: variant.GCPSEVES{}.String(),
Provider: config.ProviderConfig{GCP: &config.GCPConfig{
DeployCSIDriver: toPtr(true),
}},
Attestation: config.AttestationConfig{GCPSEVES: &config.GCPSEVES{
Measurements: measurements.M{1: measurements.WithAllBytes(0xAA, measurements.Enforce)},
}},
},
valuesModifier: prepareGCPValues,
ccmImage: "ccmImageForGCP",
},
"OpenStack": {
config: &config.Config{
AttestationVariant: variant.QEMUVTPM{}.String(),
Provider: config.ProviderConfig{OpenStack: &config.OpenStackConfig{}},
Attestation: config.AttestationConfig{QEMUVTPM: &config.QEMUVTPM{
Measurements: measurements.M{1: measurements.WithAllBytes(0xAA, measurements.Enforce)},
}},
},
valuesModifier: prepareOpenStackValues,
ccmImage: "ccmImageForOpenStack",
},
"QEMU": {
config: &config.Config{
AttestationVariant: variant.QEMUVTPM{}.String(),
Provider: config.ProviderConfig{QEMU: &config.QEMUConfig{}},
Attestation: config.AttestationConfig{QEMUVTPM: &config.QEMUVTPM{
Measurements: measurements.M{1: measurements.WithAllBytes(0xAA, measurements.Enforce)},
}},
},
valuesModifier: prepareQEMUValues,
},
@ -133,7 +142,7 @@ func TestConstellationServices(t *testing.T) {
require.NoError(err)
values, err := chartLoader.loadConstellationServicesValues()
require.NoError(err)
err = extendConstellationServicesValues(values, tc.config, []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), "https://192.0.2.1:8080/maa")
err = extendConstellationServicesValues(values, tc.config, []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
require.NoError(err)
options := chartutil.ReleaseOptions{
@ -313,12 +322,7 @@ func prepareAWSValues(values map[string]any) error {
if !ok {
return errors.New("missing 'join-service' key")
}
m := measurements.M{1: measurements.WithAllBytes(0xAA, false)}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
ccmVals, ok := values["ccm"].(map[string]any)
@ -347,13 +351,7 @@ func prepareAzureValues(values map[string]any) error {
if !ok {
return errors.New("missing 'join-service' key")
}
joinVals["idkeydigests"] = "[\"baaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaad\", \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]"
m := measurements.M{1: measurements.WithAllBytes(0xAA, false)}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
ccmVals, ok := values["ccm"].(map[string]any)
@ -459,14 +457,6 @@ func prepareGCPValues(values map[string]any) error {
return errors.New("missing 'join-service' key")
}
m := measurements.M{
1: measurements.WithAllBytes(0xAA, measurements.Enforce),
}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
ccmVals, ok := values["ccm"].(map[string]any)
@ -535,12 +525,7 @@ func prepareOpenStackValues(values map[string]any) error {
if !ok {
return errors.New("missing 'join-service' key")
}
m := measurements.M{1: measurements.WithAllBytes(0xAA, measurements.Enforce)}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
ccmVals, ok := values["ccm"].(map[string]any)
@ -570,12 +555,7 @@ func prepareQEMUValues(values map[string]any) error {
if !ok {
return errors.New("missing 'join-service' key")
}
m := measurements.M{1: measurements.WithAllBytes(0xAA, measurements.Enforce)}
mJSON, err := json.Marshal(m)
if err != nil {
return err
}
joinVals["measurements"] = string(mJSON)
joinVals["measurementSalt"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
verificationVals, ok := values["verification-service"].(map[string]any)

View File

@ -4,6 +4,6 @@ metadata:
name: join-config
namespace: testNamespace
data:
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
attestationConfig: "{\"measurements\":{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}}"
binaryData:
measurementSalt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View File

@ -47,11 +47,6 @@ spec:
- name: config
projected:
sources:
- configMap:
items:
- key: measurements
path: measurements
name: join-config
- secret:
items:
- key: mastersecret

View File

@ -4,7 +4,6 @@ metadata:
name: join-config
namespace: testNamespace
data:
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
idKeyConfig: "{\"acceptedKeyDigests\":[\"baaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaadbaaaaaad\",\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"enforcementPolicy\":\"Equal\",\"maaURL\":\"https://192.0.2.1:8080/maa\"}"
attestationConfig: "{\"measurements\":{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}},\"bootloaderVersion\":0,\"teeVersion\":0,\"snpVersion\":0,\"microcodeVersion\":0,\"firmwareSignerConfig\":{\"acceptedKeyDigests\":[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"enforcementPolicy\":\"MAAFallback\",\"maaURL\":\"https://192.0.2.1:8080/maa\"},\"amdRootKey\":\"-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE-----\\n\"}"
binaryData:
measurementSalt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View File

@ -47,11 +47,6 @@ spec:
- name: config
projected:
sources:
- configMap:
items:
- key: measurements
path: measurements
name: join-config
- secret:
items:
- key: mastersecret

View File

@ -4,6 +4,6 @@ metadata:
name: join-config
namespace: testNamespace
data:
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
attestationConfig: "{\"measurements\":{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}}"
binaryData:
measurementSalt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View File

@ -47,11 +47,6 @@ spec:
- name: config
projected:
sources:
- configMap:
items:
- key: measurements
path: measurements
name: join-config
- secret:
items:
- key: mastersecret

View File

@ -4,6 +4,6 @@ metadata:
name: join-config
namespace: testNamespace
data:
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
attestationConfig: "{\"measurements\":{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}}"
binaryData:
measurementSalt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View File

@ -47,11 +47,6 @@ spec:
- name: config
projected:
sources:
- configMap:
items:
- key: measurements
path: measurements
name: join-config
- secret:
items:
- key: mastersecret

View File

@ -4,6 +4,6 @@ metadata:
name: join-config
namespace: testNamespace
data:
measurements: "{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}"
attestationConfig: "{\"measurements\":{\"1\":{\"expected\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"warnOnly\":false}}}"
binaryData:
measurementSalt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View File

@ -47,11 +47,6 @@ spec:
- name: config
projected:
sources:
- configMap:
items:
- key: measurements
path: measurements
name: join-config
- secret:
items:
- key: mastersecret

View File

@ -31,7 +31,6 @@ go_test(
"//internal/cloud/cloudprovider",
"//internal/config",
"//internal/file",
"//internal/variant",
"//internal/versionsapi",
"@com_github_spf13_afero//:afero",
"@com_github_stretchr_testify//assert",

View File

@ -92,11 +92,7 @@ func imageVariant(provider cloudprovider.Provider, config *config.Config) (strin
case cloudprovider.AWS:
return config.Provider.AWS.Region, nil
case cloudprovider.Azure:
attestVariant, err := variant.FromString(config.AttestationVariant)
if err != nil {
return "", fmt.Errorf("parsing attestation variant: %w", err)
}
if attestVariant.Equal(variant.AzureTrustedLaunch{}) {
if config.GetAttestationConfig().GetVariant().Equal(variant.AzureTrustedLaunch{}) {
return "trustedlaunch", nil
}
return "cvm", nil

View File

@ -16,7 +16,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
@ -126,16 +125,16 @@ func TestImageVariant(t *testing.T) {
"Azure cvm": {
csp: cloudprovider.Azure,
config: &config.Config{
AttestationVariant: variant.AzureSEVSNP{}.String(),
Image: "someImage", Provider: config.ProviderConfig{Azure: &config.AzureConfig{}},
Attestation: config.AttestationConfig{AzureSEVSNP: &config.AzureSEVSNP{}},
},
wantVariant: "cvm",
},
"Azure trustedlaunch": {
csp: cloudprovider.Azure,
config: &config.Config{
AttestationVariant: variant.AzureTrustedLaunch{}.String(),
Image: "someImage", Provider: config.ProviderConfig{Azure: &config.AzureConfig{}},
Attestation: config.AttestationConfig{AzureTrustedLaunch: &config.AzureTrustedLaunch{}},
},
wantVariant: "trustedlaunch",
},

View File

@ -19,6 +19,7 @@ go_library(
"//internal/constants",
"//internal/kubernetes",
"//internal/kubernetes/kubectl",
"//internal/variant",
"//internal/versions",
"//internal/versions/components",
"//internal/versionsapi",

View File

@ -22,6 +22,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/constants"
internalk8s "github.com/edgelesssys/constellation/v2/internal/kubernetes"
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/edgelesssys/constellation/v2/internal/versions/components"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
@ -40,6 +41,10 @@ import (
// ErrInProgress signals that an upgrade is in progress inside the cluster.
var ErrInProgress = errors.New("upgrade in progress")
// ErrLegacyJoinConfig signals that a legacy join-config was found.
// TODO: v2.9 remove.
var ErrLegacyJoinConfig = errors.New("legacy join-config with missing attestationConfig found")
// GetConstellationVersion queries the constellation-version object for a given field.
func GetConstellationVersion(ctx context.Context, client DynamicInterface) (updatev1alpha1.NodeVersion, error) {
raw, err := client.GetCurrent(ctx, "constellation-version")
@ -164,10 +169,6 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config)
return errors.Join(upgradeErrs...)
}
if err := u.UpdateMeasurements(ctx, conf.GetMeasurements()); err != nil {
return fmt.Errorf("updating measurements: %w", err)
}
updatedNodeVersion, err := u.applyNodeVersion(ctx, nodeVersion)
if err != nil {
return fmt.Errorf("applying upgrade: %w", err)
@ -209,51 +210,67 @@ func (u *Upgrader) CurrentKubernetesVersion(ctx context.Context) (string, error)
return nodeVersion.Spec.KubernetesClusterVersion, nil
}
// UpdateMeasurements fetches the cluster's measurements, compares them to a set of new measurements
// and updates the cluster's measurements if they are different from the new ones.
func (u *Upgrader) UpdateMeasurements(ctx context.Context, newMeasurements measurements.M) error {
currentMeasurements, existingConf, err := u.GetClusterMeasurements(ctx)
// UpdateAttestationConfig fetches the cluster's attestation config, compares them to a new config,
// and updates the cluster's config if it is different from the new one.
func (u *Upgrader) UpdateAttestationConfig(ctx context.Context, newAttestConfig config.AttestationCfg) error {
currentAttestConfig, joinConfig, err := u.GetClusterAttestationConfig(ctx, newAttestConfig.GetVariant())
if err != nil {
return fmt.Errorf("getting cluster measurements: %w", err)
if !errors.Is(err, ErrLegacyJoinConfig) {
return fmt.Errorf("getting cluster attestation config: %w", err)
}
if currentMeasurements.EqualTo(newMeasurements) {
fmt.Fprintln(u.outWriter, "Cluster is already using the chosen measurements, skipping measurements upgrade")
currentAttestConfig, joinConfig, err = joinConfigMigration(joinConfig, newAttestConfig.GetVariant())
if err != nil {
return fmt.Errorf("migrating join config: %w", err)
}
}
equal, err := newAttestConfig.EqualTo(currentAttestConfig)
if err != nil {
return fmt.Errorf("comparing attestation configs: %w", err)
}
if equal {
fmt.Fprintln(u.outWriter, "Cluster is already using the chosen attestation config, skipping config upgrade")
return nil
}
// backup of previous measurements
existingConf.Data["oldMeasurements"] = existingConf.Data[constants.MeasurementsFilename]
joinConfig.Data[constants.AttestationConfigFilename+"_backup"] = joinConfig.Data[constants.AttestationConfigFilename]
measurementsJSON, err := json.Marshal(newMeasurements)
newConfigJSON, err := json.Marshal(newAttestConfig)
if err != nil {
return fmt.Errorf("marshaling measurements: %w", err)
return fmt.Errorf("marshaling attestation config: %w", err)
}
existingConf.Data[constants.MeasurementsFilename] = string(measurementsJSON)
u.log.Debugf("Triggering measurements config map update now")
if _, err = u.stableInterface.updateConfigMap(ctx, existingConf); err != nil {
return fmt.Errorf("setting new measurements: %w", err)
joinConfig.Data[constants.AttestationConfigFilename] = string(newConfigJSON)
u.log.Debugf("Triggering attestation config update now")
if _, err = u.stableInterface.updateConfigMap(ctx, joinConfig); err != nil {
return fmt.Errorf("setting new attestation config: %w", err)
}
fmt.Fprintln(u.outWriter, "Successfully updated the cluster's expected measurements")
fmt.Fprintln(u.outWriter, "Successfully updated the cluster's attestation config")
return nil
}
// GetClusterMeasurements fetches the join-config configmap from the cluster, extracts the measurements
// and returns both the full configmap and the measurements.
func (u *Upgrader) GetClusterMeasurements(ctx context.Context) (measurements.M, *corev1.ConfigMap, error) {
// GetClusterAttestationConfig fetches the join-config configmap from the cluster, extracts the config
// and returns both the full configmap and the attestation config.
func (u *Upgrader) GetClusterAttestationConfig(ctx context.Context, variant variant.Variant) (config.AttestationCfg, *corev1.ConfigMap, error) {
existingConf, err := u.stableInterface.getCurrentConfigMap(ctx, constants.JoinConfigMap)
if err != nil {
return measurements.M{}, &corev1.ConfigMap{}, fmt.Errorf("retrieving current measurements: %w", err)
return nil, nil, fmt.Errorf("retrieving current attestation config: %w", err)
}
if _, ok := existingConf.Data[constants.MeasurementsFilename]; !ok {
return measurements.M{}, &corev1.ConfigMap{}, errors.New("measurements missing from join-config")
if _, ok := existingConf.Data[constants.AttestationConfigFilename]; !ok {
// TODO: v2.9 remove legacy config detection since it is only required for upgrades from v2.7
if _, ok := existingConf.Data["measurements"]; ok {
u.log.Debugf("Legacy join config detected, migrating to new config")
return nil, existingConf, ErrLegacyJoinConfig
}
var currentMeasurements measurements.M
if err := json.Unmarshal([]byte(existingConf.Data[constants.MeasurementsFilename]), &currentMeasurements); err != nil {
return measurements.M{}, &corev1.ConfigMap{}, fmt.Errorf("retrieving current measurements: %w", err)
return nil, nil, errors.New("attestation config missing from join-config")
}
return currentMeasurements, existingConf, nil
existingAttestationConfig, err := config.UnmarshalAttestationConfig([]byte(existingConf.Data[constants.AttestationConfigFilename]), variant)
if err != nil {
return nil, nil, fmt.Errorf("retrieving current attestation config: %w", err)
}
return existingAttestationConfig, existingConf, nil
}
// applyComponentsCM applies the k8s components ConfigMap to the cluster.
@ -416,6 +433,45 @@ func (u *stableClient) kubernetesVersion() (string, error) {
return serverVersion.GitVersion, nil
}
// joinConfigMigration prepares a join-config ConfigMap for migration from v2.7 to v2.8.
// TODO: v2.9: remove this function.
func joinConfigMigration(existingConf *corev1.ConfigMap, attestVariant variant.Variant) (config.AttestationCfg, *corev1.ConfigMap, error) {
m, ok := existingConf.Data["measurements"]
if !ok {
return nil, nil, errors.New("no measurements found in configmap")
}
var measurements measurements.M
if err := json.Unmarshal([]byte(m), &measurements); err != nil {
return nil, nil, fmt.Errorf("unmarshalling measurements: %w", err)
}
var oldConf config.AttestationCfg
switch attestVariant {
case variant.AWSNitroTPM{}:
oldConf = &config.AWSNitroTPM{}
case variant.AzureSEVSNP{}:
oldConf = &config.AzureSEVSNP{}
case variant.AzureTrustedLaunch{}:
oldConf = &config.AzureTrustedLaunch{}
case variant.GCPSEVES{}:
oldConf = &config.GCPSEVES{}
case variant.QEMUVTPM{}:
oldConf = &config.QEMUVTPM{}
default:
return nil, nil, fmt.Errorf("unknown variant: %s", attestVariant)
}
oldConf.SetMeasurements(measurements)
oldConfJSON, err := json.Marshal(oldConf)
if err != nil {
return nil, nil, fmt.Errorf("marshalling previous config: %w", err)
}
existingConf.Data[constants.AttestationConfigFilename] = string(oldConfJSON)
return oldConf, existingConf, nil
}
type helmInterface interface {
Upgrade(ctx context.Context, config *config.Config, timeout time.Duration, allowDestructive bool) error
}

View File

@ -253,64 +253,82 @@ func TestUpdateMeasurements(t *testing.T) {
someErr := errors.New("error")
testCases := map[string]struct {
updater *stubStableClient
newMeasurements measurements.M
newConfig config.AttestationCfg
wantUpdate bool
wantErr bool
}{
"success": {
updater: &stubStableClient{
configMaps: map[string]*corev1.ConfigMap{
constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.JoinConfigMap: newJoinConfigMap(`{"measurements":{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}}`),
},
},
newMeasurements: measurements.M{
newConfig: &config.GCPSEVES{
Measurements: measurements.M{
0: measurements.WithAllBytes(0xBB, measurements.Enforce),
},
},
wantUpdate: true,
},
"measurements are the same": {
updater: &stubStableClient{
configMaps: map[string]*corev1.ConfigMap{
constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.JoinConfigMap: newJoinConfigMap(`{"measurements":{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}}`),
},
},
newMeasurements: measurements.M{
newConfig: &config.GCPSEVES{
Measurements: measurements.M{
0: measurements.WithAllBytes(0xAA, measurements.Enforce),
},
},
},
"setting warnOnly to true is allowed": {
updater: &stubStableClient{
configMaps: map[string]*corev1.ConfigMap{
constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.JoinConfigMap: newJoinConfigMap(`{"measurements":{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}}`),
},
},
newMeasurements: measurements.M{
newConfig: &config.GCPSEVES{
Measurements: measurements.M{
0: measurements.WithAllBytes(0xAA, measurements.WarnOnly),
},
},
wantUpdate: true,
},
"setting warnOnly to false is allowed": {
updater: &stubStableClient{
configMaps: map[string]*corev1.ConfigMap{
constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":true}}`),
constants.JoinConfigMap: newJoinConfigMap(`{"measurements":{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":true}}}`),
},
},
newMeasurements: measurements.M{
newConfig: &config.GCPSEVES{
Measurements: measurements.M{
0: measurements.WithAllBytes(0xAA, measurements.Enforce),
},
},
wantUpdate: true,
},
"getCurrent error": {
updater: &stubStableClient{getErr: someErr},
newConfig: &config.GCPSEVES{
Measurements: measurements.M{
0: measurements.WithAllBytes(0xBB, measurements.Enforce),
},
},
wantErr: true,
},
"update error": {
updater: &stubStableClient{
configMaps: map[string]*corev1.ConfigMap{
constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.JoinConfigMap: newJoinConfigMap(`{"measurements":{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}}`),
},
updateErr: someErr,
},
newConfig: &config.GCPSEVES{
Measurements: measurements.M{
0: measurements.WithAllBytes(0xBB, measurements.Enforce),
},
},
wantErr: true,
},
}
@ -325,7 +343,7 @@ func TestUpdateMeasurements(t *testing.T) {
log: logger.NewTest(t),
}
err := upgrader.UpdateMeasurements(context.Background(), tc.newMeasurements)
err := upgrader.UpdateAttestationConfig(context.Background(), tc.newConfig)
if tc.wantErr {
assert.Error(err)
return
@ -333,9 +351,9 @@ func TestUpdateMeasurements(t *testing.T) {
assert.NoError(err)
if tc.wantUpdate {
newMeasurementsJSON, err := json.Marshal(tc.newMeasurements)
newConfigJSON, err := json.Marshal(tc.newConfig)
require.NoError(t, err)
assert.JSONEq(string(newMeasurementsJSON), tc.updater.updatedConfigMaps[constants.JoinConfigMap].Data[constants.MeasurementsFilename])
assert.JSONEq(string(newConfigJSON), tc.updater.updatedConfigMaps[constants.JoinConfigMap].Data[constants.AttestationConfigFilename])
} else {
assert.Nil(tc.updater.updatedConfigMaps)
}
@ -468,7 +486,7 @@ func newJoinConfigMap(data string) *corev1.ConfigMap {
Name: constants.JoinConfigMap,
},
Data: map[string]string{
constants.MeasurementsFilename: data,
constants.AttestationConfigFilename: data,
},
}
}

View File

@ -16,6 +16,7 @@ Commands:
* [fetch-measurements](#constellation-config-fetch-measurements): Fetch measurements for configured cloud provider and image
* [instance-types](#constellation-config-instance-types): Print the supported instance types for all cloud providers
* [kubernetes-versions](#constellation-config-kubernetes-versions): Print the Kubernetes versions supported by this CLI
* [migrate](#constellation-config-migrate): Migrate a configuration file to a new version
* [create](#constellation-create): Create instances on a cloud platform for your Constellation cluster
* [init](#constellation-init): Initialize the Constellation cluster
* [mini](#constellation-mini): Manage MiniConstellation clusters
@ -173,6 +174,33 @@ constellation config kubernetes-versions [flags]
--tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE")
```
## constellation config migrate
Migrate a configuration file to a new version
### Synopsis
Migrate a configuration file to a new version.
```
constellation config migrate [flags]
```
### Options
```
-h, --help help for migrate
```
### Options inherited from parent commands
```
--config string path to the configuration file (default "constellation-conf.yaml")
--debug enable debug logging
--force disable version compatibility checks - might result in corrupted clusters
--tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE")
```
## constellation create
Create instances on a cloud platform for your Constellation cluster

View File

@ -1,6 +1,14 @@
# Configuration migrations
This document describes breaking changes in the configuration file format between Constellation releases.
Use [`constellation config migrate`](./cli.md#constellation-config-migrate) to automatically update an old config file to a new format.
## Migrating from CLI versions before 2.8
- The `measurements` field for each cloud service provider was replaced with a global `attestation` field.
- The `confidentialVM`, `idKeyDigest`, and `enforceIdKeyDigest` fields for the Azure cloud service provider were removed in favor of using the global `attestation` field.
- The optional global field `attestationVariant` was replaced by the now required `attestation` field.
## Migrating from CLI versions before 2.3

View File

@ -31,6 +31,7 @@ To learn which Kubernetes versions are supported by a particular CLI, run [const
The Constellation configuration file is located in the file `constellation-conf.yaml` in your workspace.
Refer to the [migration reference](../reference/config-migration.md) to check if you need to update fields in your configuration file.
Use [`constellation config migrate`](../reference/cli.md#constellation-config-migrate) to automatically update an old config file to a new format.
## Check for upgrades

View File

@ -91,18 +91,24 @@ func TestUpgrade(t *testing.T) {
testNodesEventuallyAvailable(t, k, *wantControl, *wantWorker)
testPodsEventuallyReady(t, k, "kube-system")
targetVersions := writeUpgradeConfig(require, *targetImage, *targetKubernetes, *targetMicroservices)
cli, err := getCLIPath(*cliPath)
require.NoError(err)
// Migrate config if necessary.
cmd := exec.CommandContext(context.Background(), cli, "config", "migrate", "--config", constants.ConfigFilename, "--force", "--debug")
msg, err := cmd.CombinedOutput()
require.NoErrorf(err, "%s", string(msg))
log.Println(string(msg))
targetVersions := writeUpgradeConfig(require, *targetImage, *targetKubernetes, *targetMicroservices)
data, err := os.ReadFile("./constellation-conf.yaml")
require.NoError(err)
log.Println(string(data))
log.Println("Triggering upgrade.")
cmd := exec.CommandContext(context.Background(), cli, "upgrade", "apply", "--force", "--debug", "-y")
msg, err := cmd.CombinedOutput()
cmd = exec.CommandContext(context.Background(), cli, "upgrade", "apply", "--force", "--debug", "-y")
msg, err = cmd.CombinedOutput()
require.NoErrorf(err, "%s", string(msg))
require.NoError(containsUnexepectedMsg(string(msg)))
log.Println(string(msg))
@ -158,7 +164,12 @@ func containsUnexepectedMsg(input string) error {
func writeUpgradeConfig(require *require.Assertions, image string, kubernetes string, microservices string) versionContainer {
fileHandler := file.NewHandler(afero.NewOsFs())
cfg, err := config.New(fileHandler, constants.ConfigFilename, true)
require.NoError(err)
var cfgErr *config.ValidationError
var longMsg string
if errors.As(err, &cfgErr) {
longMsg = cfgErr.LongMessage()
}
require.NoError(err, longMsg)
info, err := fetchUpgradeInfo(context.Background(), cfg.GetProvider(), image)
require.NoError(err)

View File

@ -30,7 +30,7 @@ type Validator struct {
}
// NewValidator create a new Validator structure and returns it.
func NewValidator(cfg config.AWSNitroTPM, log vtpm.AttestationLogger) *Validator {
func NewValidator(cfg *config.AWSNitroTPM, log vtpm.AttestationLogger) *Validator {
v := &Validator{}
v.Validator = vtpm.NewValidator(
cfg.Measurements,

View File

@ -57,7 +57,7 @@ func (e *idKeyError) Unwrap() error {
}
func (e *idKeyError) Error() string {
return fmt.Sprintf("configured idkeydigests %x don't contain reported idkeydigest %x", e.expectedValues, e.encounteredValue)
return fmt.Sprintf("accepted idkeydigest list %x doesn't contain reported idkeydigest %x", e.expectedValues, e.encounteredValue)
}
type versionError struct {

View File

@ -36,13 +36,13 @@ type Validator struct {
hclValidator hclAkValidator
maa maaValidator
config config.AzureSEVSNP
config *config.AzureSEVSNP
log vtpm.AttestationLogger
}
// NewValidator initializes a new Azure validator with the provided PCR values.
func NewValidator(cfg config.AzureSEVSNP, log vtpm.AttestationLogger) *Validator {
func NewValidator(cfg *config.AzureSEVSNP, log vtpm.AttestationLogger) *Validator {
if log == nil {
log = nopAttestationLogger{}
}

View File

@ -201,7 +201,7 @@ func TestGetAttestationCert(t *testing.T) {
},
}
validator := NewValidator(config.AzureTrustedLaunch{Measurements: measurements.M{}}, nil)
validator := NewValidator(&config.AzureTrustedLaunch{Measurements: measurements.M{}}, nil)
cert, err := x509.ParseCertificate(rootCert.Raw)
require.NoError(err)
roots := x509.NewCertPool()

View File

@ -35,7 +35,7 @@ type Validator struct {
}
// NewValidator initializes a new Azure validator with the provided PCR values.
func NewValidator(cfg config.AzureTrustedLaunch, log vtpm.AttestationLogger) *Validator {
func NewValidator(cfg *config.AzureTrustedLaunch, log vtpm.AttestationLogger) *Validator {
rootPool := x509.NewCertPool()
rootPool.AddCert(ameRoot)
v := &Validator{roots: rootPool}

View File

@ -12,7 +12,6 @@ go_library(
"//internal/attestation/azure/snp",
"//internal/attestation/azure/trustedlaunch",
"//internal/attestation/gcp",
"//internal/attestation/measurements",
"//internal/attestation/qemu",
"//internal/attestation/vtpm",
"//internal/config",
@ -25,6 +24,7 @@ go_test(
srcs = ["choose_test.go"],
embed = [":choose"],
deps = [
"//internal/attestation/measurements",
"//internal/config",
"//internal/variant",
"@com_github_stretchr_testify//assert",

View File

@ -14,7 +14,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch"
"github.com/edgelesssys/constellation/v2/internal/attestation/gcp"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/qemu"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/config"
@ -42,26 +41,21 @@ func Issuer(attestationVariant variant.Variant, log vtpm.AttestationLogger) (atl
}
// Validator returns the validator for the given variant.
func Validator(
attestationVariant variant.Variant, measurements measurements.M, idKeyCfg config.SNPFirmwareSignerConfig, log vtpm.AttestationLogger,
) (atls.Validator, error) {
switch attestationVariant {
case variant.AWSNitroTPM{}:
return aws.NewValidator(config.AWSNitroTPM{Measurements: measurements}, log), nil
case variant.AzureTrustedLaunch{}:
return trustedlaunch.NewValidator(config.AzureTrustedLaunch{Measurements: measurements}, log), nil
case variant.AzureSEVSNP{}:
cfg := config.DefaultForAzureSEVSNP()
cfg.Measurements = measurements
cfg.FirmwareSignerConfig = idKeyCfg
func Validator(cfg config.AttestationCfg, log vtpm.AttestationLogger) (atls.Validator, error) {
switch cfg := cfg.(type) {
case *config.AWSNitroTPM:
return aws.NewValidator(cfg, log), nil
case *config.AzureTrustedLaunch:
return trustedlaunch.NewValidator(cfg, log), nil
case *config.AzureSEVSNP:
return snp.NewValidator(cfg, log), nil
case variant.GCPSEVES{}:
return gcp.NewValidator(config.GCPSEVES{Measurements: measurements}, log), nil
case variant.QEMUVTPM{}:
return qemu.NewValidator(config.QEMUVTPM{Measurements: measurements}, log), nil
case variant.Dummy{}:
case *config.GCPSEVES:
return gcp.NewValidator(cfg, log), nil
case *config.QEMUVTPM:
return qemu.NewValidator(cfg, log), nil
case *config.DummyCfg:
return atls.NewFakeValidator(variant.Dummy{}), nil
default:
return nil, fmt.Errorf("unknown attestation variant: %s", attestationVariant)
return nil, fmt.Errorf("unknown attestation variant")
}
}

View File

@ -10,6 +10,7 @@ import (
"encoding/asn1"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/stretchr/testify/assert"
@ -64,29 +65,29 @@ func TestIssuer(t *testing.T) {
func TestValidator(t *testing.T) {
testCases := map[string]struct {
variant variant.Variant
cfg config.AttestationCfg
wantErr bool
}{
"aws-nitro-tpm": {
variant: variant.AWSNitroTPM{},
cfg: &config.AWSNitroTPM{},
},
"azure-sev-snp": {
variant: variant.AzureSEVSNP{},
cfg: &config.AzureSEVSNP{},
},
"azure-trusted-launch": {
variant: variant.AzureTrustedLaunch{},
cfg: &config.AzureTrustedLaunch{},
},
"gcp-sev-es": {
variant: variant.GCPSEVES{},
cfg: &config.GCPSEVES{},
},
"qemu-vtpm": {
variant: variant.QEMUVTPM{},
cfg: &config.QEMUVTPM{},
},
"dummy": {
variant: variant.Dummy{},
cfg: &config.DummyCfg{},
},
"unknown": {
variant: unknownVariant{},
cfg: unknownConfig{},
wantErr: true,
},
}
@ -96,14 +97,14 @@ func TestValidator(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
validator, err := Validator(tc.variant, nil, config.SNPFirmwareSignerConfig{}, nil)
validator, err := Validator(tc.cfg, nil)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.True(validator.OID().Equal(tc.variant.OID()))
assert.True(validator.OID().Equal(tc.cfg.GetVariant().OID()))
})
}
}
@ -121,3 +122,17 @@ func (unknownVariant) String() string {
func (unknownVariant) Equal(other variant.Getter) bool {
return other.OID().Equal(unknownVariant{}.OID())
}
type unknownConfig struct{}
func (unknownConfig) GetVariant() variant.Variant {
return unknownVariant{}
}
func (unknownConfig) GetMeasurements() measurements.M {
return nil
}
func (unknownConfig) SetMeasurements(measurements.M) {}
func (unknownConfig) EqualTo(config.AttestationCfg) (bool, error) { return false, nil }

View File

@ -35,7 +35,7 @@ type Validator struct {
}
// NewValidator initializes a new GCP validator with the provided PCR values.
func NewValidator(cfg config.GCPSEVES, log vtpm.AttestationLogger) *Validator {
func NewValidator(cfg *config.GCPSEVES, log vtpm.AttestationLogger) *Validator {
v := &Validator{
restClient: newInstanceClient,
}

View File

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
package idkeydigest
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
@ -124,6 +125,19 @@ func DefaultList() List {
}
}
// EqualTo returns true if the List of digests is equal to the other List.
func (d List) EqualTo(other List) bool {
if len(d) != len(other) {
return false
}
for i := range d {
if !bytes.Equal(d[i], other[i]) {
return false
}
}
return true
}
// MarshalYAML implements the yaml.Marshaler interface.
func (d List) MarshalYAML() (any, error) {
encodedIDKeyDigests := []string{}

View File

@ -24,7 +24,7 @@ type Validator struct {
}
// NewValidator initializes a new QEMU validator with the provided PCR values.
func NewValidator(cfg config.QEMUVTPM, log vtpm.AttestationLogger) *Validator {
func NewValidator(cfg *config.QEMUVTPM, log vtpm.AttestationLogger) *Validator {
return &Validator{
Validator: vtpm.NewValidator(
cfg.Measurements,

View File

@ -43,13 +43,11 @@ go_test(
data = glob(["testdata/**"]),
embed = [":config"],
deps = [
"//internal/attestation/idkeydigest",
"//internal/attestation/measurements",
"//internal/cloud/cloudprovider",
"//internal/config/instancetypes",
"//internal/constants",
"//internal/file",
"//internal/variant",
"@com_github_go_playground_locales//en",
"@com_github_go_playground_universal_translator//:universal-translator",
"@com_github_go_playground_validator_v10//:validator",

View File

@ -16,12 +16,44 @@ import (
"github.com/edgelesssys/constellation/v2/internal/variant"
)
// AttestationConfig is the common interface for passing attestation configs.
type AttestationConfig interface {
// AttestationCfg is the common interface for passing attestation configs.
type AttestationCfg interface {
// GetMeasurements returns the measurements that should be used for attestation.
GetMeasurements() measurements.M
// SetMeasurements updates a config's measurements using the given measurements.
SetMeasurements(m measurements.M)
// GetVariant returns the variant of the attestation config.
GetVariant() variant.Variant
// NewerThan returns true if the config is equal to the given config.
EqualTo(AttestationCfg) (bool, error)
}
// UnmarshalAttestationConfig unmarshals the config file into the correct type.
func UnmarshalAttestationConfig(data []byte, attestVariant variant.Variant) (AttestationCfg, error) {
switch attestVariant {
case variant.AWSNitroTPM{}:
return unmarshalTypedConfig[*AWSNitroTPM](data)
case variant.AzureSEVSNP{}:
return unmarshalTypedConfig[*AzureSEVSNP](data)
case variant.AzureTrustedLaunch{}:
return unmarshalTypedConfig[*AzureTrustedLaunch](data)
case variant.GCPSEVES{}:
return unmarshalTypedConfig[*GCPSEVES](data)
case variant.QEMUVTPM{}:
return unmarshalTypedConfig[*QEMUVTPM](data)
case variant.Dummy{}:
return unmarshalTypedConfig[*DummyCfg](data)
default:
return nil, fmt.Errorf("unknown variant: %s", attestVariant)
}
}
func unmarshalTypedConfig[T AttestationCfg](data []byte) (AttestationCfg, error) {
var cfg T
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return cfg, nil
}
// Certificate is a wrapper around x509.Certificate allowing custom marshaling.
@ -73,3 +105,30 @@ func mustParsePEM(data string) Certificate {
}
return cert
}
// DummyCfg is a placeholder for unknown attestation configs.
type DummyCfg struct {
// description: |
// The measurements that should be used for attestation.
Measurements measurements.M `json:"measurements,omitempty"`
}
// GetMeasurements returns the configs measurements.
func (c DummyCfg) GetMeasurements() measurements.M {
return c.Measurements
}
// GetVariant returns a dummy variant.
func (DummyCfg) GetVariant() variant.Variant {
return variant.Dummy{}
}
// SetMeasurements sets the configs measurements.
func (c *DummyCfg) SetMeasurements(m measurements.M) {
c.Measurements = m
}
// EqualTo returns true if measurements of the configs are equal.
func (c DummyCfg) EqualTo(other AttestationCfg) (bool, error) {
return c.Measurements.EqualTo(other.GetMeasurements()), nil
}

View File

@ -11,6 +11,8 @@ import (
"fmt"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
@ -19,6 +21,42 @@ import (
// testCertPEM is a certificate in PEM format used for unit tests.
var testCertPEM = `-----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`
func TestUnmarshalAttestationConfig(t *testing.T) {
testCases := map[string]struct {
cfg AttestationCfg
}{
"AWSNitroTPM": {
cfg: &AWSNitroTPM{Measurements: measurements.DefaultsFor(cloudprovider.AWS)},
},
"AzureSEVSNP": {
cfg: DefaultForAzureSEVSNP(),
},
"AzureTrustedLaunch": {
cfg: &AzureTrustedLaunch{Measurements: measurements.DefaultsFor(cloudprovider.Azure)},
},
"GCPSEVES": {
cfg: &GCPSEVES{Measurements: measurements.DefaultsFor(cloudprovider.GCP)},
},
"QEMUVTPM": {
cfg: &QEMUVTPM{Measurements: measurements.DefaultsFor(cloudprovider.QEMU)},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
out, err := json.Marshal(tc.cfg)
require.NoError(err)
cfg, err := UnmarshalAttestationConfig(out, tc.cfg.GetVariant())
require.NoError(err)
assert.Equal(tc.cfg, cfg)
})
}
}
func TestCertificateMarshalJSON(t *testing.T) {
require := require.New(t)
assert := assert.New(t)

View File

@ -19,6 +19,7 @@ All config relevant definitions, parsing and validation functions should go here
package config
import (
"bytes"
"errors"
"fmt"
"io/fs"
@ -42,17 +43,9 @@ import (
"github.com/edgelesssys/constellation/v2/internal/versions"
)
// Measurements is a required alias since docgen is not able to work with
// types in other packages.
type Measurements = measurements.M
// Digests is a required alias since docgen is not able to work with
// types in other packages.
type Digests = idkeydigest.List
const (
// Version2 is the second version number for Constellation config file.
Version2 = "v2"
// Version3 is the third version number for Constellation config file.
Version3 = "v3"
defaultName = "constell"
)
@ -61,7 +54,7 @@ const (
type Config struct {
// description: |
// Schema version of this configuration file.
Version string `yaml:"version" validate:"eq=v2"`
Version string `yaml:"version" validate:"eq=v3"`
// description: |
// Machine image version used to create Constellation nodes.
Image string `yaml:"image" validate:"required,version_compatibility"`
@ -81,11 +74,11 @@ type Config struct {
// DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md
DebugCluster *bool `yaml:"debugCluster" validate:"required"`
// description: |
// Attestation variant used to verify the integrity of a node.
AttestationVariant string `yaml:"attestationVariant,omitempty" validate:"valid_attestation_variant"` // TODO: v2.8: Mark required
// description: |
// Supported cloud providers and their specific configurations.
Provider ProviderConfig `yaml:"provider" validate:"dive"`
// description: |
// Configuration for attestation validation. This configuration provides sensible defaults for the Constellation version it was created for.\nSee our docs for an overview on attestation: https://docs.edgeless.systems/constellation/architecture/attestation
Attestation AttestationConfig `yaml:"attestation" validate:"dive"`
}
// ProviderConfig are cloud-provider specific configuration values used by the CLI.
@ -129,9 +122,6 @@ type AWSConfig struct {
// description: |
// Name of the IAM profile to use for the worker nodes.
IAMProfileWorkerNodes string `yaml:"iamProfileWorkerNodes" validate:"required"`
// description: |
// Expected VM measurements.
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// AzureConfig are Azure specific configuration values used by the CLI.
@ -167,20 +157,8 @@ type AzureConfig struct {
// Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage
DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"`
// description: |
// Use Confidential VMs. Always needs to be true.
ConfidentialVM *bool `yaml:"confidentialVM,omitempty" validate:"omitempty,deprecated"` // TODO: v2.8 remove
// description: |
// Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob.
SecureBoot *bool `yaml:"secureBoot" validate:"required"`
// description: |
// List of accepted values for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf
IDKeyDigest Digests `yaml:"idKeyDigest" validate:"required_if=EnforceIdKeyDigest true,omitempty"`
// description: |
// Enforce the specified idKeyDigest value during remote attestation.
EnforceIDKeyDigest idkeydigest.Enforcement `yaml:"enforceIdKeyDigest" validate:"required"`
// description: |
// Expected confidential VM measurements.
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// GCPConfig are GCP specific configuration values used by the CLI.
@ -206,9 +184,6 @@ type GCPConfig struct {
// description: |
// Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage
DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"`
// description: |
// Expected confidential VM measurements.
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// OpenStackConfig holds config information for OpenStack based Constellation deployments.
@ -255,9 +230,6 @@ type OpenStackConfig struct {
// description: |
// If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack.
DirectDownload *bool `yaml:"directDownload" validate:"required"`
// description: |
// Measurement used to enable measured boot.
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// QEMUConfig holds config information for QEMU based Constellation deployments.
@ -286,15 +258,34 @@ type QEMUConfig struct {
// description: |
// Path to the OVMF firmware. Leave empty for auto selection.
Firmware string `yaml:"firmware"`
}
// AttestationConfig configuration values used for attestation.
// Fields should remain pointer-types so custom specific configs can nil them
// if not required.
type AttestationConfig struct {
// description: |
// Measurement used to enable measured boot.
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
// AWS Nitro TPM attestation.
AWSNitroTPM *AWSNitroTPM `yaml:"awsNitroTPM,omitempty" validate:"omitempty,dive"`
// description: |
// Azure SEV-SNP attestation.\nSee our docs for more information on configurable values
// TODO(AB#3071): add link after docs are written
AzureSEVSNP *AzureSEVSNP `yaml:"azureSEVSNP,omitempty" validate:"omitempty,dive"`
// description: |
// Azure TPM attestation (Trusted Launch).
AzureTrustedLaunch *AzureTrustedLaunch `yaml:"azureTrustedLaunch,omitempty" validate:"omitempty,dive"`
// description: |
// GCP SEV-ES attestation.
GCPSEVES *GCPSEVES `yaml:"gcpSEVES,omitempty" validate:"omitempty,dive"`
// description: |
// QEMU vTPM attestation.
QEMUVTPM *QEMUVTPM `yaml:"qemuVTPM,omitempty" validate:"omitempty,dive"`
}
// Default returns a struct with the default config.
func Default() *Config {
return &Config{
Version: Version2,
Version: Version3,
Image: defaultImage,
Name: defaultName,
MicroserviceVersion: compatibility.EnsurePrefixV(constants.VersionInfo()),
@ -308,7 +299,6 @@ func Default() *Config {
StateDiskType: "gp3",
IAMProfileControlPlane: "",
IAMProfileWorkerNodes: "",
Measurements: measurements.DefaultsFor(cloudprovider.AWS),
},
Azure: &AzureConfig{
SubscriptionID: "",
@ -319,10 +309,7 @@ func Default() *Config {
InstanceType: "Standard_DC4as_v5",
StateDiskType: "Premium_LRS",
DeployCSIDriver: toPtr(true),
IDKeyDigest: idkeydigest.DefaultList(),
EnforceIDKeyDigest: idkeydigest.MAAFallback,
SecureBoot: toPtr(false),
Measurements: measurements.DefaultsFor(cloudprovider.Azure),
},
GCP: &GCPConfig{
Project: "",
@ -332,11 +319,9 @@ func Default() *Config {
InstanceType: "n2d-standard-4",
StateDiskType: "pd-ssd",
DeployCSIDriver: toPtr(true),
Measurements: measurements.DefaultsFor(cloudprovider.GCP),
},
OpenStack: &OpenStackConfig{
DirectDownload: toPtr(true),
Measurements: measurements.DefaultsFor(cloudprovider.OpenStack),
},
QEMU: &QEMUConfig{
ImageFormat: "raw",
@ -346,9 +331,15 @@ func Default() *Config {
LibvirtURI: "",
LibvirtContainerImage: imageversion.Libvirt(),
NVRAM: "production",
Measurements: measurements.DefaultsFor(cloudprovider.QEMU),
},
},
Attestation: AttestationConfig{
AWSNitroTPM: &AWSNitroTPM{Measurements: measurements.DefaultsFor(cloudprovider.AWS)},
AzureSEVSNP: DefaultForAzureSEVSNP(),
AzureTrustedLaunch: &AzureTrustedLaunch{Measurements: measurements.DefaultsFor(cloudprovider.Azure)},
GCPSEVES: &GCPSEVES{Measurements: measurements.DefaultsFor(cloudprovider.GCP)},
QEMUVTPM: &QEMUVTPM{Measurements: measurements.DefaultsFor(cloudprovider.QEMU)},
},
}
}
@ -389,7 +380,7 @@ func New(fileHandler file.Handler, name string, force bool) (*Config, error) {
}
// Backwards compatibility: configs without the field `microserviceVersion` are valid in version 2.6.
// In case the field is not set in an old config we prefil it with the default value.
// In case the field is not set in an old config we prefill it with the default value.
if c.MicroserviceVersion == "" {
c.MicroserviceVersion = Default().MicroserviceVersion
}
@ -415,21 +406,21 @@ func (c *Config) HasProvider(provider cloudprovider.Provider) bool {
}
// UpdateMeasurements overwrites measurements in config with the provided ones.
func (c *Config) UpdateMeasurements(newMeasurements Measurements) {
if c.Provider.AWS != nil {
c.Provider.AWS.Measurements.CopyFrom(newMeasurements)
func (c *Config) UpdateMeasurements(newMeasurements measurements.M) {
if c.Attestation.AWSNitroTPM != nil {
c.Attestation.AWSNitroTPM.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.Azure != nil {
c.Provider.Azure.Measurements.CopyFrom(newMeasurements)
if c.Attestation.AzureSEVSNP != nil {
c.Attestation.AzureSEVSNP.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.GCP != nil {
c.Provider.GCP.Measurements.CopyFrom(newMeasurements)
if c.Attestation.AzureTrustedLaunch != nil {
c.Attestation.AzureTrustedLaunch.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.OpenStack != nil {
c.Provider.OpenStack.Measurements.CopyFrom(newMeasurements)
if c.Attestation.GCPSEVES != nil {
c.Attestation.GCPSEVES.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.QEMU != nil {
c.Provider.QEMU.Measurements.CopyFrom(newMeasurements)
if c.Attestation.QEMUVTPM != nil {
c.Attestation.QEMUVTPM.Measurements.CopyFrom(newMeasurements)
}
}
@ -439,19 +430,30 @@ func (c *Config) UpdateMeasurements(newMeasurements Measurements) {
func (c *Config) RemoveProviderExcept(provider cloudprovider.Provider) {
currentProviderConfigs := c.Provider
c.Provider = ProviderConfig{}
// TODO(AB#2976): Replace attestation replacement
// with custom function for attestation selection
currentAttetationConfigs := c.Attestation
c.Attestation = AttestationConfig{}
switch provider {
case cloudprovider.AWS:
c.Provider.AWS = currentProviderConfigs.AWS
c.Attestation.AWSNitroTPM = currentAttetationConfigs.AWSNitroTPM
case cloudprovider.Azure:
c.Provider.Azure = currentProviderConfigs.Azure
c.Attestation.AzureSEVSNP = currentAttetationConfigs.AzureSEVSNP
case cloudprovider.GCP:
c.Provider.GCP = currentProviderConfigs.GCP
c.Attestation.GCPSEVES = currentAttetationConfigs.GCPSEVES
case cloudprovider.OpenStack:
c.Provider.OpenStack = currentProviderConfigs.OpenStack
c.Attestation.QEMUVTPM = currentAttetationConfigs.QEMUVTPM
case cloudprovider.QEMU:
c.Provider.QEMU = currentProviderConfigs.QEMU
c.Attestation.QEMUVTPM = currentAttetationConfigs.QEMUVTPM
default:
c.Provider = currentProviderConfigs
c.Attestation = currentAttetationConfigs
}
}
@ -488,40 +490,31 @@ func (c *Config) GetProvider() cloudprovider.Provider {
return cloudprovider.Unknown
}
// GetMeasurements returns the configured measurements or nil if no provder is set.
func (c *Config) GetMeasurements() measurements.M {
if c.Provider.AWS != nil {
return c.Provider.AWS.Measurements
// GetAttestationConfig returns the configured attestation config.
func (c *Config) GetAttestationConfig() AttestationCfg {
if c.Attestation.AWSNitroTPM != nil {
return c.Attestation.AWSNitroTPM
}
if c.Provider.Azure != nil {
return c.Provider.Azure.Measurements
if c.Attestation.AzureSEVSNP != nil {
return c.Attestation.AzureSEVSNP
}
if c.Provider.GCP != nil {
return c.Provider.GCP.Measurements
if c.Attestation.AzureTrustedLaunch != nil {
return c.Attestation.AzureTrustedLaunch
}
if c.Provider.OpenStack != nil {
return c.Provider.OpenStack.Measurements
if c.Attestation.GCPSEVES != nil {
return c.Attestation.GCPSEVES
}
if c.Provider.QEMU != nil {
return c.Provider.QEMU.Measurements
if c.Attestation.QEMUVTPM != nil {
return c.Attestation.QEMUVTPM
}
return nil
return &DummyCfg{}
}
// IDKeyDigestPolicy returns the IDKeyDigest checking policy for a cloud provider.
func (c *Config) IDKeyDigestPolicy() idkeydigest.Enforcement {
if c.Provider.Azure != nil {
return c.Provider.Azure.EnforceIDKeyDigest
// UpdateMAAURL updates the MAA URL in the config.
func (c *Config) UpdateMAAURL(maaURL string) {
if c.Attestation.AzureSEVSNP != nil {
c.Attestation.AzureSEVSNP.FirmwareSignerConfig.MAAURL = maaURL
}
return idkeydigest.Unknown
}
// IDKeyDigests returns the ID Key Digests for the configured cloud provider.
func (c *Config) IDKeyDigests() idkeydigest.List {
if c.Provider.Azure != nil {
return c.Provider.Azure.IDKeyDigest
}
return nil
}
// DeployCSIDriver returns whether the CSI driver should be deployed for a given cloud provider.
@ -587,10 +580,6 @@ func (c *Config) Validate(force bool) error {
return err
}
if err := validate.RegisterTranslation("valid_attestation_variant", trans, registerValidAttestVariantError, c.translateValidAttestVariantError); err != nil {
return err
}
if err := validate.RegisterValidation("valid_name", c.validateName); err != nil {
return err
}
@ -627,10 +616,6 @@ func (c *Config) Validate(force bool) error {
return err
}
if err := validate.RegisterValidation("valid_attestation_variant", c.validAttestVariant); err != nil {
return err
}
if err := validate.RegisterValidation("deprecated", warnDeprecated); err != nil {
return err
}
@ -638,6 +623,16 @@ func (c *Config) Validate(force bool) error {
// Register provider validation
validate.RegisterStructValidation(validateProvider, ProviderConfig{})
// Register Attestation validation error types
if err := validate.RegisterTranslation("no_attestation", trans, registerNoAttestationError, translateNoAttestationError); err != nil {
return err
}
if err := validate.RegisterTranslation("more_than_one_attestation", trans, registerMoreThanOneAttestationError, c.translateMoreThanOneAttestationError); err != nil {
return err
}
validate.RegisterStructValidation(validateAttestation, AttestationConfig{})
err := validate.Struct(c)
if err == nil {
return nil
@ -660,7 +655,7 @@ func (c *Config) Validate(force bool) error {
type AWSNitroTPM struct {
// description: |
// Expected TPM measurements.
Measurements measurements.M `json:"measurements" yaml:"measurements"`
Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"`
}
// GetVariant returns aws-nitro-tpm as the variant.
@ -673,11 +668,25 @@ func (c AWSNitroTPM) GetMeasurements() measurements.M {
return c.Measurements
}
// SetMeasurements updates a config's measurements using the given measurements.
func (c *AWSNitroTPM) SetMeasurements(m measurements.M) {
c.Measurements = m
}
// EqualTo returns true if the config is equal to the given config.
func (c AWSNitroTPM) EqualTo(other AttestationCfg) (bool, error) {
otherCfg, ok := other.(*AWSNitroTPM)
if !ok {
return false, fmt.Errorf("cannot compare %T with %T", c, other)
}
return c.Measurements.EqualTo(otherCfg.Measurements), nil
}
// AzureSEVSNP is the configuration for Azure SEV-SNP attestation.
type AzureSEVSNP struct {
// description: |
// Expected confidential VM measurements.
Measurements measurements.M `json:"measurements" yaml:"measurements"`
// Expected TPM measurements.
Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"`
// description: |
// Lowest acceptable bootloader version.
BootloaderVersion uint8 `json:"bootloaderVersion" yaml:"bootloaderVersion"`
@ -701,8 +710,8 @@ type AzureSEVSNP struct {
// DefaultForAzureSEVSNP returns the default configuration for Azure SEV-SNP attestation.
// Version numbers are hard coded and should be updated with each new release.
// TODO(AB#3042): replace with dynamic lookup for configurable values.
func DefaultForAzureSEVSNP() AzureSEVSNP {
return AzureSEVSNP{
func DefaultForAzureSEVSNP() *AzureSEVSNP {
return &AzureSEVSNP{
Measurements: measurements.DefaultsFor(cloudprovider.Azure),
BootloaderVersion: 2,
TEEVersion: 0,
@ -727,6 +736,29 @@ func (c AzureSEVSNP) GetMeasurements() measurements.M {
return c.Measurements
}
// SetMeasurements updates a config's measurements using the given measurements.
func (c *AzureSEVSNP) SetMeasurements(m measurements.M) {
c.Measurements = m
}
// EqualTo returns true if the config is equal to the given config.
func (c AzureSEVSNP) EqualTo(old AttestationCfg) (bool, error) {
otherCfg, ok := old.(*AzureSEVSNP)
if !ok {
return false, fmt.Errorf("cannot compare %T with %T", c, old)
}
firmwareSignerCfgEqual := c.FirmwareSignerConfig.EqualTo(otherCfg.FirmwareSignerConfig)
measurementsEqual := c.Measurements.EqualTo(otherCfg.Measurements)
bootloaderEqual := c.BootloaderVersion == otherCfg.BootloaderVersion
teeEqual := c.TEEVersion == otherCfg.TEEVersion
snpEqual := c.SNPVersion == otherCfg.SNPVersion
microcodeEqual := c.MicrocodeVersion == otherCfg.MicrocodeVersion
rootKeyEqual := bytes.Equal(c.AMDRootKey.Raw, otherCfg.AMDRootKey.Raw)
return firmwareSignerCfgEqual && measurementsEqual && bootloaderEqual && teeEqual && snpEqual && microcodeEqual && rootKeyEqual, nil
}
// SNPFirmwareSignerConfig is the configuration for validating the firmware signer.
type SNPFirmwareSignerConfig struct {
// description: |
@ -740,11 +772,16 @@ type SNPFirmwareSignerConfig struct {
MAAURL string `json:"maaURL,omitempty" yaml:"maaURL,omitempty" validate:"len=0"`
}
// EqualTo returns true if the config is equal to the given config.
func (c SNPFirmwareSignerConfig) EqualTo(other SNPFirmwareSignerConfig) bool {
return c.AcceptedKeyDigests.EqualTo(other.AcceptedKeyDigests) && c.EnforcementPolicy == other.EnforcementPolicy && c.MAAURL == other.MAAURL
}
// AzureTrustedLaunch is the configuration for Azure Trusted Launch attestation.
type AzureTrustedLaunch struct {
// description: |
// Expected TPM measurements.
Measurements measurements.M `json:"measurements" yaml:"measurements"`
Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"`
}
// GetVariant returns azure-trusted-launch as the variant.
@ -757,11 +794,25 @@ func (c AzureTrustedLaunch) GetMeasurements() measurements.M {
return c.Measurements
}
// SetMeasurements updates a config's measurements using the given measurements.
func (c *AzureTrustedLaunch) SetMeasurements(m measurements.M) {
c.Measurements = m
}
// EqualTo returns true if the config is equal to the given config.
func (c AzureTrustedLaunch) EqualTo(other AttestationCfg) (bool, error) {
otherCfg, ok := other.(*AzureTrustedLaunch)
if !ok {
return false, fmt.Errorf("cannot compare %T with %T", c, other)
}
return c.Measurements.EqualTo(otherCfg.Measurements), nil
}
// GCPSEVES is the configuration for GCP SEV-ES attestation.
type GCPSEVES struct {
// description: |
// Expected TPM measurements.
Measurements measurements.M `json:"measurements" yaml:"measurements"`
Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"`
}
// GetVariant returns gcp-sev-es as the variant.
@ -774,11 +825,25 @@ func (c GCPSEVES) GetMeasurements() measurements.M {
return c.Measurements
}
// SetMeasurements updates a config's measurements using the given measurements.
func (c *GCPSEVES) SetMeasurements(m measurements.M) {
c.Measurements = m
}
// EqualTo returns true if the config is equal to the given config.
func (c GCPSEVES) EqualTo(other AttestationCfg) (bool, error) {
otherCfg, ok := other.(*GCPSEVES)
if !ok {
return false, fmt.Errorf("cannot compare %T with %T", c, other)
}
return c.Measurements.EqualTo(otherCfg.Measurements), nil
}
// QEMUVTPM is the configuration for QEMU vTPM attestation.
type QEMUVTPM struct {
// description: |
// Expected TPM measurements.
Measurements measurements.M `json:"measurements" yaml:"measurements"`
Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"`
}
// GetVariant returns qemu-vtpm as the variant.
@ -791,6 +856,20 @@ func (c QEMUVTPM) GetMeasurements() measurements.M {
return c.Measurements
}
// SetMeasurements updates a config's measurements using the given measurements.
func (c *QEMUVTPM) SetMeasurements(m measurements.M) {
c.Measurements = m
}
// EqualTo returns true if the config is equal to the given config.
func (c QEMUVTPM) EqualTo(other AttestationCfg) (bool, error) {
otherCfg, ok := other.(*QEMUVTPM)
if !ok {
return false, fmt.Errorf("cannot compare %T with %T", c, other)
}
return c.Measurements.EqualTo(otherCfg.Measurements), nil
}
func toPtr[T any](v T) *T {
return &v
}

View File

@ -18,6 +18,7 @@ var (
GCPConfigDoc encoder.Doc
OpenStackConfigDoc encoder.Doc
QEMUConfigDoc encoder.Doc
AttestationConfigDoc encoder.Doc
AWSNitroTPMDoc encoder.Doc
AzureSEVSNPDoc encoder.Doc
SNPFirmwareSignerConfigDoc encoder.Doc
@ -66,16 +67,16 @@ func init() {
ConfigDoc.Fields[6].Note = ""
ConfigDoc.Fields[6].Description = "DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md"
ConfigDoc.Fields[6].Comments[encoder.LineComment] = "DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md"
ConfigDoc.Fields[7].Name = "attestationVariant"
ConfigDoc.Fields[7].Type = "string"
ConfigDoc.Fields[7].Note = "TODO: v2.8: Mark required\n"
ConfigDoc.Fields[7].Description = "Attestation variant used to verify the integrity of a node."
ConfigDoc.Fields[7].Comments[encoder.LineComment] = "Attestation variant used to verify the integrity of a node."
ConfigDoc.Fields[8].Name = "provider"
ConfigDoc.Fields[8].Type = "ProviderConfig"
ConfigDoc.Fields[7].Name = "provider"
ConfigDoc.Fields[7].Type = "ProviderConfig"
ConfigDoc.Fields[7].Note = ""
ConfigDoc.Fields[7].Description = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[7].Comments[encoder.LineComment] = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[8].Name = "attestation"
ConfigDoc.Fields[8].Type = "AttestationConfig"
ConfigDoc.Fields[8].Note = ""
ConfigDoc.Fields[8].Description = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[8].Comments[encoder.LineComment] = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[8].Description = "Configuration for attestation validation. This configuration provides sensible defaults for the Constellation version it was created for.\nSee our docs for an overview on attestation: https://docs.edgeless.systems/constellation/architecture/attestation"
ConfigDoc.Fields[8].Comments[encoder.LineComment] = "Configuration for attestation validation. This configuration provides sensible defaults for the Constellation version it was created for.\nSee our docs for an overview on attestation: https://docs.edgeless.systems/constellation/architecture/attestation"
ProviderConfigDoc.Type = "ProviderConfig"
ProviderConfigDoc.Comments[encoder.LineComment] = "ProviderConfig are cloud-provider specific configuration values used by the CLI."
@ -122,7 +123,7 @@ func init() {
FieldName: "aws",
},
}
AWSConfigDoc.Fields = make([]encoder.Doc, 7)
AWSConfigDoc.Fields = make([]encoder.Doc, 6)
AWSConfigDoc.Fields[0].Name = "region"
AWSConfigDoc.Fields[0].Type = "string"
AWSConfigDoc.Fields[0].Note = ""
@ -153,11 +154,6 @@ func init() {
AWSConfigDoc.Fields[5].Note = ""
AWSConfigDoc.Fields[5].Description = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[5].Comments[encoder.LineComment] = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[6].Name = "measurements"
AWSConfigDoc.Fields[6].Type = "Measurements"
AWSConfigDoc.Fields[6].Note = ""
AWSConfigDoc.Fields[6].Description = "Expected VM measurements."
AWSConfigDoc.Fields[6].Comments[encoder.LineComment] = "Expected VM measurements."
AzureConfigDoc.Type = "AzureConfig"
AzureConfigDoc.Comments[encoder.LineComment] = "AzureConfig are Azure specific configuration values used by the CLI."
@ -168,7 +164,7 @@ func init() {
FieldName: "azure",
},
}
AzureConfigDoc.Fields = make([]encoder.Doc, 15)
AzureConfigDoc.Fields = make([]encoder.Doc, 11)
AzureConfigDoc.Fields[0].Name = "subscription"
AzureConfigDoc.Fields[0].Type = "string"
AzureConfigDoc.Fields[0].Note = ""
@ -219,31 +215,11 @@ func init() {
AzureConfigDoc.Fields[9].Note = ""
AzureConfigDoc.Fields[9].Description = "Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
AzureConfigDoc.Fields[9].Comments[encoder.LineComment] = "Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
AzureConfigDoc.Fields[10].Name = "confidentialVM"
AzureConfigDoc.Fields[10].Name = "secureBoot"
AzureConfigDoc.Fields[10].Type = "bool"
AzureConfigDoc.Fields[10].Note = "TODO: v2.8 remove\n"
AzureConfigDoc.Fields[10].Description = "Use Confidential VMs. Always needs to be true."
AzureConfigDoc.Fields[10].Comments[encoder.LineComment] = "Use Confidential VMs. Always needs to be true."
AzureConfigDoc.Fields[11].Name = "secureBoot"
AzureConfigDoc.Fields[11].Type = "bool"
AzureConfigDoc.Fields[11].Note = ""
AzureConfigDoc.Fields[11].Description = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
AzureConfigDoc.Fields[11].Comments[encoder.LineComment] = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
AzureConfigDoc.Fields[12].Name = "idKeyDigest"
AzureConfigDoc.Fields[12].Type = "Digests"
AzureConfigDoc.Fields[12].Note = ""
AzureConfigDoc.Fields[12].Description = "List of accepted values for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf"
AzureConfigDoc.Fields[12].Comments[encoder.LineComment] = "List of accepted values for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf"
AzureConfigDoc.Fields[13].Name = "enforceIdKeyDigest"
AzureConfigDoc.Fields[13].Type = "Enforcement"
AzureConfigDoc.Fields[13].Note = ""
AzureConfigDoc.Fields[13].Description = "Enforce the specified idKeyDigest value during remote attestation."
AzureConfigDoc.Fields[13].Comments[encoder.LineComment] = "Enforce the specified idKeyDigest value during remote attestation."
AzureConfigDoc.Fields[14].Name = "measurements"
AzureConfigDoc.Fields[14].Type = "Measurements"
AzureConfigDoc.Fields[14].Note = ""
AzureConfigDoc.Fields[14].Description = "Expected confidential VM measurements."
AzureConfigDoc.Fields[14].Comments[encoder.LineComment] = "Expected confidential VM measurements."
AzureConfigDoc.Fields[10].Note = ""
AzureConfigDoc.Fields[10].Description = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
AzureConfigDoc.Fields[10].Comments[encoder.LineComment] = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
GCPConfigDoc.Type = "GCPConfig"
GCPConfigDoc.Comments[encoder.LineComment] = "GCPConfig are GCP specific configuration values used by the CLI."
@ -254,7 +230,7 @@ func init() {
FieldName: "gcp",
},
}
GCPConfigDoc.Fields = make([]encoder.Doc, 8)
GCPConfigDoc.Fields = make([]encoder.Doc, 7)
GCPConfigDoc.Fields[0].Name = "project"
GCPConfigDoc.Fields[0].Type = "string"
GCPConfigDoc.Fields[0].Note = ""
@ -290,11 +266,6 @@ func init() {
GCPConfigDoc.Fields[6].Note = ""
GCPConfigDoc.Fields[6].Description = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
GCPConfigDoc.Fields[6].Comments[encoder.LineComment] = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
GCPConfigDoc.Fields[7].Name = "measurements"
GCPConfigDoc.Fields[7].Type = "Measurements"
GCPConfigDoc.Fields[7].Note = ""
GCPConfigDoc.Fields[7].Description = "Expected confidential VM measurements."
GCPConfigDoc.Fields[7].Comments[encoder.LineComment] = "Expected confidential VM measurements."
OpenStackConfigDoc.Type = "OpenStackConfig"
OpenStackConfigDoc.Comments[encoder.LineComment] = "OpenStackConfig holds config information for OpenStack based Constellation deployments."
@ -305,7 +276,7 @@ func init() {
FieldName: "openstack",
},
}
OpenStackConfigDoc.Fields = make([]encoder.Doc, 15)
OpenStackConfigDoc.Fields = make([]encoder.Doc, 14)
OpenStackConfigDoc.Fields[0].Name = "cloud"
OpenStackConfigDoc.Fields[0].Type = "string"
OpenStackConfigDoc.Fields[0].Note = ""
@ -376,11 +347,6 @@ func init() {
OpenStackConfigDoc.Fields[13].Note = ""
OpenStackConfigDoc.Fields[13].Description = "If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack."
OpenStackConfigDoc.Fields[13].Comments[encoder.LineComment] = "If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack."
OpenStackConfigDoc.Fields[14].Name = "measurements"
OpenStackConfigDoc.Fields[14].Type = "Measurements"
OpenStackConfigDoc.Fields[14].Note = ""
OpenStackConfigDoc.Fields[14].Description = "Measurement used to enable measured boot."
OpenStackConfigDoc.Fields[14].Comments[encoder.LineComment] = "Measurement used to enable measured boot."
QEMUConfigDoc.Type = "QEMUConfig"
QEMUConfigDoc.Comments[encoder.LineComment] = "QEMUConfig holds config information for QEMU based Constellation deployments."
@ -391,7 +357,7 @@ func init() {
FieldName: "qemu",
},
}
QEMUConfigDoc.Fields = make([]encoder.Doc, 9)
QEMUConfigDoc.Fields = make([]encoder.Doc, 8)
QEMUConfigDoc.Fields[0].Name = "imageFormat"
QEMUConfigDoc.Fields[0].Type = "string"
QEMUConfigDoc.Fields[0].Note = ""
@ -432,15 +398,52 @@ func init() {
QEMUConfigDoc.Fields[7].Note = ""
QEMUConfigDoc.Fields[7].Description = "Path to the OVMF firmware. Leave empty for auto selection."
QEMUConfigDoc.Fields[7].Comments[encoder.LineComment] = "Path to the OVMF firmware. Leave empty for auto selection."
QEMUConfigDoc.Fields[8].Name = "measurements"
QEMUConfigDoc.Fields[8].Type = "Measurements"
QEMUConfigDoc.Fields[8].Note = ""
QEMUConfigDoc.Fields[8].Description = "Measurement used to enable measured boot."
QEMUConfigDoc.Fields[8].Comments[encoder.LineComment] = "Measurement used to enable measured boot."
AttestationConfigDoc.Type = "AttestationConfig"
AttestationConfigDoc.Comments[encoder.LineComment] = "AttestationConfig configuration values used for attestation."
AttestationConfigDoc.Description = "AttestationConfig configuration values used for attestation.\nFields should remain pointer-types so custom specific configs can nil them\nif not required.\n"
AttestationConfigDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "Config",
FieldName: "attestation",
},
}
AttestationConfigDoc.Fields = make([]encoder.Doc, 5)
AttestationConfigDoc.Fields[0].Name = "awsNitroTPM"
AttestationConfigDoc.Fields[0].Type = "AWSNitroTPM"
AttestationConfigDoc.Fields[0].Note = ""
AttestationConfigDoc.Fields[0].Description = "AWS Nitro TPM attestation."
AttestationConfigDoc.Fields[0].Comments[encoder.LineComment] = "AWS Nitro TPM attestation."
AttestationConfigDoc.Fields[1].Name = "azureSEVSNP"
AttestationConfigDoc.Fields[1].Type = "AzureSEVSNP"
AttestationConfigDoc.Fields[1].Note = ""
AttestationConfigDoc.Fields[1].Description = "Azure SEV-SNP attestation.\nSee our docs for more information on configurable values\nTODO(AB#3071): add link after docs are written"
AttestationConfigDoc.Fields[1].Comments[encoder.LineComment] = "Azure SEV-SNP attestation.\nSee our docs for more information on configurable values"
AttestationConfigDoc.Fields[2].Name = "azureTrustedLaunch"
AttestationConfigDoc.Fields[2].Type = "AzureTrustedLaunch"
AttestationConfigDoc.Fields[2].Note = ""
AttestationConfigDoc.Fields[2].Description = "Azure TPM attestation (Trusted Launch)."
AttestationConfigDoc.Fields[2].Comments[encoder.LineComment] = "Azure TPM attestation (Trusted Launch)."
AttestationConfigDoc.Fields[3].Name = "gcpSEVES"
AttestationConfigDoc.Fields[3].Type = "GCPSEVES"
AttestationConfigDoc.Fields[3].Note = ""
AttestationConfigDoc.Fields[3].Description = "GCP SEV-ES attestation."
AttestationConfigDoc.Fields[3].Comments[encoder.LineComment] = "GCP SEV-ES attestation."
AttestationConfigDoc.Fields[4].Name = "qemuVTPM"
AttestationConfigDoc.Fields[4].Type = "QEMUVTPM"
AttestationConfigDoc.Fields[4].Note = ""
AttestationConfigDoc.Fields[4].Description = "QEMU vTPM attestation."
AttestationConfigDoc.Fields[4].Comments[encoder.LineComment] = "QEMU vTPM attestation."
AWSNitroTPMDoc.Type = "AWSNitroTPM"
AWSNitroTPMDoc.Comments[encoder.LineComment] = "AWSNitroTPM is the configuration for AWS Nitro TPM attestation."
AWSNitroTPMDoc.Description = "AWSNitroTPM is the configuration for AWS Nitro TPM attestation."
AWSNitroTPMDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "AttestationConfig",
FieldName: "awsNitroTPM",
},
}
AWSNitroTPMDoc.Fields = make([]encoder.Doc, 1)
AWSNitroTPMDoc.Fields[0].Name = "measurements"
AWSNitroTPMDoc.Fields[0].Type = "M"
@ -451,12 +454,18 @@ func init() {
AzureSEVSNPDoc.Type = "AzureSEVSNP"
AzureSEVSNPDoc.Comments[encoder.LineComment] = "AzureSEVSNP is the configuration for Azure SEV-SNP attestation."
AzureSEVSNPDoc.Description = "AzureSEVSNP is the configuration for Azure SEV-SNP attestation."
AzureSEVSNPDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "AttestationConfig",
FieldName: "azureSEVSNP",
},
}
AzureSEVSNPDoc.Fields = make([]encoder.Doc, 7)
AzureSEVSNPDoc.Fields[0].Name = "measurements"
AzureSEVSNPDoc.Fields[0].Type = "M"
AzureSEVSNPDoc.Fields[0].Note = ""
AzureSEVSNPDoc.Fields[0].Description = "Expected confidential VM measurements."
AzureSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected confidential VM measurements."
AzureSEVSNPDoc.Fields[0].Description = "Expected TPM measurements."
AzureSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements."
AzureSEVSNPDoc.Fields[1].Name = "bootloaderVersion"
AzureSEVSNPDoc.Fields[1].Type = "uint8"
AzureSEVSNPDoc.Fields[1].Note = ""
@ -517,6 +526,12 @@ func init() {
AzureTrustedLaunchDoc.Type = "AzureTrustedLaunch"
AzureTrustedLaunchDoc.Comments[encoder.LineComment] = "AzureTrustedLaunch is the configuration for Azure Trusted Launch attestation."
AzureTrustedLaunchDoc.Description = "AzureTrustedLaunch is the configuration for Azure Trusted Launch attestation."
AzureTrustedLaunchDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "AttestationConfig",
FieldName: "azureTrustedLaunch",
},
}
AzureTrustedLaunchDoc.Fields = make([]encoder.Doc, 1)
AzureTrustedLaunchDoc.Fields[0].Name = "measurements"
AzureTrustedLaunchDoc.Fields[0].Type = "M"
@ -527,6 +542,12 @@ func init() {
GCPSEVESDoc.Type = "GCPSEVES"
GCPSEVESDoc.Comments[encoder.LineComment] = "GCPSEVES is the configuration for GCP SEV-ES attestation."
GCPSEVESDoc.Description = "GCPSEVES is the configuration for GCP SEV-ES attestation."
GCPSEVESDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "AttestationConfig",
FieldName: "gcpSEVES",
},
}
GCPSEVESDoc.Fields = make([]encoder.Doc, 1)
GCPSEVESDoc.Fields[0].Name = "measurements"
GCPSEVESDoc.Fields[0].Type = "M"
@ -537,6 +558,12 @@ func init() {
QEMUVTPMDoc.Type = "QEMUVTPM"
QEMUVTPMDoc.Comments[encoder.LineComment] = "QEMUVTPM is the configuration for QEMU vTPM attestation."
QEMUVTPMDoc.Description = "QEMUVTPM is the configuration for QEMU vTPM attestation."
QEMUVTPMDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "AttestationConfig",
FieldName: "qemuVTPM",
},
}
QEMUVTPMDoc.Fields = make([]encoder.Doc, 1)
QEMUVTPMDoc.Fields[0].Name = "measurements"
QEMUVTPMDoc.Fields[0].Type = "M"
@ -573,6 +600,10 @@ func (_ QEMUConfig) Doc() *encoder.Doc {
return &QEMUConfigDoc
}
func (_ AttestationConfig) Doc() *encoder.Doc {
return &AttestationConfigDoc
}
func (_ AWSNitroTPM) Doc() *encoder.Doc {
return &AWSNitroTPMDoc
}
@ -610,6 +641,7 @@ func GetConfigurationDoc() *encoder.FileDoc {
&GCPConfigDoc,
&OpenStackConfigDoc,
&QEMUConfigDoc,
&AttestationConfigDoc,
&AWSNitroTPMDoc,
&AzureSEVSNPDoc,
&SNPFirmwareSignerConfigDoc,

View File

@ -18,13 +18,11 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/variant"
)
func TestMain(m *testing.M) {
@ -66,11 +64,11 @@ func TestFromFile(t *testing.T) {
},
"custom config from default file": {
config: &Config{
Version: Version2,
Version: Version3,
},
configName: constants.ConfigFilename,
wantResult: &Config{
Version: Version2,
Version: Version3,
},
},
"modify default config": {
@ -124,14 +122,15 @@ func TestNewWithDefaultOptions(t *testing.T) {
c := Default()
c.RemoveProviderExcept(cloudprovider.Azure)
c.Image = "v" + constants.VersionInfo()
c.AttestationVariant = variant.AzureSEVSNP{}.String()
c.Provider.Azure.SubscriptionID = "f4278079-288c-4766-a98c-ab9d5dba01a5"
c.Provider.Azure.TenantID = "d4ff9d63-6d6d-4042-8f6a-21e804add5aa"
c.Provider.Azure.Location = "westus"
c.Provider.Azure.ResourceGroup = "test"
c.Provider.Azure.UserAssignedIdentity = "/subscriptions/8b8bd01f-efd9-4113-9bd1-c82137c32da7/resourcegroups/constellation-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/constellation-identity"
c.Provider.Azure.AppClientID = "3ea4bdc1-1cc1-4237-ae78-0831eff3491e"
c.Provider.Azure.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, measurements.Enforce)}
c.Attestation.AzureSEVSNP.Measurements = measurements.M{
0: measurements.WithAllBytes(0x00, measurements.Enforce),
}
return c
}(),
envToSet: map[string]string{
@ -144,7 +143,6 @@ func TestNewWithDefaultOptions(t *testing.T) {
c := Default()
c.RemoveProviderExcept(cloudprovider.Azure)
c.Image = "v" + constants.VersionInfo()
c.AttestationVariant = variant.AzureSEVSNP{}.String()
c.Provider.Azure.SubscriptionID = "f4278079-288c-4766-a98c-ab9d5dba01a5"
c.Provider.Azure.TenantID = "d4ff9d63-6d6d-4042-8f6a-21e804add5aa"
c.Provider.Azure.Location = "westus"
@ -152,7 +150,9 @@ func TestNewWithDefaultOptions(t *testing.T) {
c.Provider.Azure.ClientSecretValue = "other-value" // < Note secret set in config, as well.
c.Provider.Azure.UserAssignedIdentity = "/subscriptions/8b8bd01f-efd9-4113-9bd1-c82137c32da7/resourcegroups/constellation-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/constellation-identity"
c.Provider.Azure.AppClientID = "3ea4bdc1-1cc1-4237-ae78-0831eff3491e"
c.Provider.Azure.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, measurements.Enforce)}
c.Attestation.AzureSEVSNP.Measurements = measurements.M{
0: measurements.WithAllBytes(0x00, measurements.Enforce),
}
return c
}(),
envToSet: map[string]string{
@ -188,7 +188,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
}
func TestValidate(t *testing.T) {
const defaultErrCount = 33 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const defaultErrCount = 34 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const azErrCount = 9
const gcpErrCount = 6
@ -224,9 +224,7 @@ func TestValidate(t *testing.T) {
"default Azure config is not valid": {
cnf: func() *Config {
cnf := Default()
az := cnf.Provider.Azure
cnf.Provider = ProviderConfig{}
cnf.Provider.Azure = az
cnf.RemoveProviderExcept(cloudprovider.Azure)
return cnf
}(),
wantErr: true,
@ -235,8 +233,8 @@ func TestValidate(t *testing.T) {
"Azure config with all required fields is valid": {
cnf: func() *Config {
cnf := Default()
cnf.RemoveProviderExcept(cloudprovider.Azure)
cnf.Image = "v" + constants.VersionInfo()
cnf.AttestationVariant = variant.AzureSEVSNP{}.String()
az := cnf.Provider.Azure
az.SubscriptionID = "01234567-0123-0123-0123-0123456789ab"
az.TenantID = "01234567-0123-0123-0123-0123456789ab"
@ -247,16 +245,16 @@ func TestValidate(t *testing.T) {
az.ClientSecretValue = "test-client-secret"
cnf.Provider = ProviderConfig{}
cnf.Provider.Azure = az
cnf.Provider.Azure.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, measurements.Enforce)}
cnf.Attestation.AzureSEVSNP.Measurements = measurements.M{
0: measurements.WithAllBytes(0x00, measurements.Enforce),
}
return cnf
}(),
},
"default GCP config is not valid": {
cnf: func() *Config {
cnf := Default()
gcp := cnf.Provider.GCP
cnf.Provider = ProviderConfig{}
cnf.Provider.GCP = gcp
cnf.RemoveProviderExcept(cloudprovider.GCP)
return cnf
}(),
wantErr: true,
@ -265,8 +263,8 @@ func TestValidate(t *testing.T) {
"GCP config with all required fields is valid": {
cnf: func() *Config {
cnf := Default()
cnf.RemoveProviderExcept(cloudprovider.GCP)
cnf.Image = "v" + constants.VersionInfo()
cnf.AttestationVariant = variant.GCPSEVES{}.String()
gcp := cnf.Provider.GCP
gcp.Region = "test-region"
gcp.Project = "test-project"
@ -274,7 +272,9 @@ func TestValidate(t *testing.T) {
gcp.ServiceAccountKeyPath = "test-key-path"
cnf.Provider = ProviderConfig{}
cnf.Provider.GCP = gcp
cnf.Provider.GCP.Measurements = measurements.M{15: measurements.WithAllBytes(0x00, measurements.Enforce)}
cnf.Attestation.GCPSEVES.Measurements = measurements.M{
0: measurements.WithAllBytes(0x00, measurements.Enforce),
}
return cnf
}(),
},
@ -405,38 +405,38 @@ func TestConfig_UpdateMeasurements(t *testing.T) {
{ // AWS
conf := Default()
conf.RemoveProviderExcept(cloudprovider.AWS)
for k := range conf.Provider.AWS.Measurements {
delete(conf.Provider.AWS.Measurements, k)
for k := range conf.Attestation.AWSNitroTPM.Measurements {
delete(conf.Attestation.AWSNitroTPM.Measurements, k)
}
conf.UpdateMeasurements(newMeasurements)
assert.Equal(newMeasurements, conf.Provider.AWS.Measurements)
assert.Equal(newMeasurements, conf.Attestation.AWSNitroTPM.Measurements)
}
{ // Azure
conf := Default()
conf.RemoveProviderExcept(cloudprovider.Azure)
for k := range conf.Provider.Azure.Measurements {
delete(conf.Provider.Azure.Measurements, k)
for k := range conf.Attestation.AzureSEVSNP.Measurements {
delete(conf.Attestation.AzureSEVSNP.Measurements, k)
}
conf.UpdateMeasurements(newMeasurements)
assert.Equal(newMeasurements, conf.Provider.Azure.Measurements)
assert.Equal(newMeasurements, conf.Attestation.AzureSEVSNP.Measurements)
}
{ // GCP
conf := Default()
conf.RemoveProviderExcept(cloudprovider.GCP)
for k := range conf.Provider.GCP.Measurements {
delete(conf.Provider.GCP.Measurements, k)
for k := range conf.Attestation.GCPSEVES.Measurements {
delete(conf.Attestation.GCPSEVES.Measurements, k)
}
conf.UpdateMeasurements(newMeasurements)
assert.Equal(newMeasurements, conf.Provider.GCP.Measurements)
assert.Equal(newMeasurements, conf.Attestation.GCPSEVES.Measurements)
}
{ // QEMU
conf := Default()
conf.RemoveProviderExcept(cloudprovider.QEMU)
for k := range conf.Provider.QEMU.Measurements {
delete(conf.Provider.QEMU.Measurements, k)
for k := range conf.Attestation.QEMUVTPM.Measurements {
delete(conf.Attestation.QEMUVTPM.Measurements, k)
}
conf.UpdateMeasurements(newMeasurements)
assert.Equal(newMeasurements, conf.Provider.QEMU.Measurements)
assert.Equal(newMeasurements, conf.Attestation.QEMUVTPM.Measurements)
}
}
@ -715,71 +715,11 @@ func TestValidateProvider(t *testing.T) {
}
func TestConfigVersionCompatibility(t *testing.T) {
t.Skip() // TODO(daniel-weisse): re-enable and re-write for config v3
testCases := map[string]struct {
config string
expectedConfig *Config
}{
"config v2 azure with singular idkeydigest": {
config: "testdata/configAzureV2SingleIDKeyDigest.yaml",
expectedConfig: &Config{
Version: "v2",
Image: "v2.5.0",
StateDiskSizeGB: 16,
KubernetesVersion: "1.23",
DebugCluster: toPtr(false),
Provider: ProviderConfig{
Azure: &AzureConfig{
SubscriptionID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
TenantID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
Location: "West Europe",
ResourceGroup: "resourceGroup",
UserAssignedIdentity: "/subscriptions/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ConstellationUAMI",
AppClientID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
ClientSecretValue: "aaaaaaaaaaaaaaaaaaaa",
StateDiskType: "Premium_LRS",
ConfidentialVM: toPtr(true),
InstanceType: "Standard_DC4as_v5",
IDKeyDigest: idkeydigest.List{{0x03, 0x56, 0x21, 0x58, 0x82, 0xa8, 0x25, 0x27, 0x9a, 0x85, 0xb3, 0x00, 0xb0, 0xb7, 0x42, 0x93, 0x1d, 0x11, 0x3b, 0xf7, 0xe3, 0x2d, 0xde, 0x2e, 0x50, 0xff, 0xde, 0x7e, 0xc7, 0x43, 0xca, 0x49, 0x1e, 0xcd, 0xd7, 0xf3, 0x36, 0xdc, 0x28, 0xa6, 0xe0, 0xb2, 0xbb, 0x57, 0xaf, 0x7a, 0x44, 0xa3}},
EnforceIDKeyDigest: idkeydigest.WarnOnly,
SecureBoot: toPtr(false),
DeployCSIDriver: toPtr(true),
Measurements: measurements.DefaultsFor(cloudprovider.Azure),
},
},
},
},
"config v2 azure with multiple idkeydigest": {
config: "testdata/configAzureV2MultipleIDKeyDigest.yaml",
expectedConfig: &Config{
Version: "v2",
Image: "v2.5.0",
StateDiskSizeGB: 16,
KubernetesVersion: "1.23",
DebugCluster: toPtr(false),
Provider: ProviderConfig{
Azure: &AzureConfig{
SubscriptionID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
TenantID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
Location: "West Europe",
ResourceGroup: "resourceGroup",
UserAssignedIdentity: "/subscriptions/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ConstellationUAMI",
AppClientID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
ClientSecretValue: "aaaaaaaaaaaaaaaaaaaa",
StateDiskType: "Premium_LRS",
ConfidentialVM: toPtr(true),
InstanceType: "Standard_DC4as_v5",
IDKeyDigest: idkeydigest.List{
{0x57, 0x48, 0x6a, 0x44, 0x7e, 0xc0, 0xf1, 0x95, 0x80, 0x02, 0xa2, 0x2a, 0x06, 0xb7, 0x67, 0x3b, 0x9f, 0xd2, 0x7d, 0x11, 0xe1, 0xc6, 0x52, 0x74, 0x98, 0x05, 0x60, 0x54, 0xc5, 0xfa, 0x92, 0xd2, 0x3c, 0x50, 0xf9, 0xde, 0x44, 0x07, 0x27, 0x60, 0xfe, 0x2b, 0x6f, 0xb8, 0x97, 0x40, 0xb6, 0x96},
{0x03, 0x56, 0x21, 0x58, 0x82, 0xa8, 0x25, 0x27, 0x9a, 0x85, 0xb3, 0x00, 0xb0, 0xb7, 0x42, 0x93, 0x1d, 0x11, 0x3b, 0xf7, 0xe3, 0x2d, 0xde, 0x2e, 0x50, 0xff, 0xde, 0x7e, 0xc7, 0x43, 0xca, 0x49, 0x1e, 0xcd, 0xd7, 0xf3, 0x36, 0xdc, 0x28, 0xa6, 0xe0, 0xb2, 0xbb, 0x57, 0xaf, 0x7a, 0x44, 0xa3},
},
EnforceIDKeyDigest: idkeydigest.WarnOnly,
SecureBoot: toPtr(false),
DeployCSIDriver: toPtr(true),
Measurements: measurements.DefaultsFor(cloudprovider.Azure),
},
},
},
},
"config v2 gcp": {
config: "testdata/configGCPV2.yaml",
expectedConfig: &Config{
@ -797,7 +737,6 @@ func TestConfigVersionCompatibility(t *testing.T) {
InstanceType: "n2d-standard-4",
StateDiskType: "pd-ssd",
DeployCSIDriver: toPtr(true),
Measurements: measurements.DefaultsFor(cloudprovider.GCP),
},
},
},
@ -818,7 +757,6 @@ func TestConfigVersionCompatibility(t *testing.T) {
StateDiskType: "gp2",
IAMProfileControlPlane: "control_plane_instance_profile",
IAMProfileWorkerNodes: "node_instance_profile",
Measurements: measurements.DefaultsFor(cloudprovider.AWS),
},
},
},

View File

@ -0,0 +1,15 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "migration",
srcs = ["migration.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/config/migration",
visibility = ["//:__subpackages__"],
deps = [
"//internal/attestation/idkeydigest",
"//internal/attestation/measurements",
"//internal/config",
"//internal/file",
"//internal/variant",
],
)

View File

@ -0,0 +1,242 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// Package migration contains outdated configuration formats and their migration functions.
package migration
import (
"fmt"
"os"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/variant"
)
const (
// Version2 is the second version number for Constellation config file.
Version2 = "v2"
)
// Config defines configuration used by CLI.
type Config struct {
Version string `yaml:"version" validate:"eq=v2"`
Image string `yaml:"image" validate:"required,version_compatibility"`
Name string `yaml:"name" validate:"valid_name,required"`
StateDiskSizeGB int `yaml:"stateDiskSizeGB" validate:"min=0"`
KubernetesVersion string `yaml:"kubernetesVersion" validate:"required,supported_k8s_version"`
MicroserviceVersion string `yaml:"microserviceVersion" validate:"required,version_compatibility"`
DebugCluster *bool `yaml:"debugCluster" validate:"required"`
AttestationVariant string `yaml:"attestationVariant,omitempty" validate:"valid_attestation_variant"`
Provider ProviderConfig `yaml:"provider" validate:"dive"`
}
// ProviderConfig are cloud-provider specific configuration values used by the CLI.
// Fields should remain pointer-types so custom specific configs can nil them
// if not required.
type ProviderConfig struct {
AWS *AWSConfig `yaml:"aws,omitempty" validate:"omitempty,dive"`
Azure *AzureConfig `yaml:"azure,omitempty" validate:"omitempty,dive"`
GCP *GCPConfig `yaml:"gcp,omitempty" validate:"omitempty,dive"`
OpenStack *OpenStackConfig `yaml:"openstack,omitempty" validate:"omitempty,dive"`
QEMU *QEMUConfig `yaml:"qemu,omitempty" validate:"omitempty,dive"`
}
// AWSConfig are AWS specific configuration values used by the CLI.
type AWSConfig struct {
Region string `yaml:"region" validate:"required"`
Zone string `yaml:"zone" validate:"required"`
InstanceType string `yaml:"instanceType" validate:"lowercase,aws_instance_type"`
StateDiskType string `yaml:"stateDiskType" validate:"oneof=standard gp2 gp3 st1 sc1 io1"`
IAMProfileControlPlane string `yaml:"iamProfileControlPlane" validate:"required"`
IAMProfileWorkerNodes string `yaml:"iamProfileWorkerNodes" validate:"required"`
Measurements measurements.M `yaml:"measurements" validate:"required,no_placeholders"`
}
// AzureConfig are Azure specific configuration values used by the CLI.
type AzureConfig struct {
SubscriptionID string `yaml:"subscription" validate:"uuid"`
TenantID string `yaml:"tenant" validate:"uuid"`
Location string `yaml:"location" validate:"required"`
ResourceGroup string `yaml:"resourceGroup" validate:"required"`
UserAssignedIdentity string `yaml:"userAssignedIdentity" validate:"required"`
AppClientID string `yaml:"appClientID" validate:"uuid"`
ClientSecretValue string `yaml:"clientSecretValue" validate:"required"`
InstanceType string `yaml:"instanceType" validate:"azure_instance_type"`
StateDiskType string `yaml:"stateDiskType" validate:"oneof=Premium_LRS Premium_ZRS Standard_LRS StandardSSD_LRS StandardSSD_ZRS"`
DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"`
ConfidentialVM *bool `yaml:"confidentialVM,omitempty" validate:"omitempty,deprecated"`
SecureBoot *bool `yaml:"secureBoot" validate:"required"`
IDKeyDigest idkeydigest.List `yaml:"idKeyDigest" validate:"required_if=EnforceIdKeyDigest true,omitempty"`
EnforceIDKeyDigest idkeydigest.Enforcement `yaml:"enforceIdKeyDigest" validate:"required"`
Measurements measurements.M `yaml:"measurements" validate:"required,no_placeholders"`
}
// GCPConfig are GCP specific configuration values used by the CLI.
type GCPConfig struct {
Project string `yaml:"project" validate:"required"`
Region string `yaml:"region" validate:"required"`
Zone string `yaml:"zone" validate:"required"`
ServiceAccountKeyPath string `yaml:"serviceAccountKeyPath" validate:"required"`
InstanceType string `yaml:"instanceType" validate:"gcp_instance_type"`
StateDiskType string `yaml:"stateDiskType" validate:"oneof=pd-standard pd-balanced pd-ssd"`
DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"`
Measurements measurements.M `yaml:"measurements" validate:"required,no_placeholders"`
}
// OpenStackConfig holds config information for OpenStack based Constellation deployments.
type OpenStackConfig struct {
Cloud string `yaml:"cloud"`
AvailabilityZone string `yaml:"availabilityZone" validate:"required"`
FlavorID string `yaml:"flavorID" validate:"required"`
FloatingIPPoolID string `yaml:"floatingIPPoolID" validate:"required"`
AuthURL string `yaml:"authURL" validate:"required"`
ProjectID string `yaml:"projectID" validate:"required"`
ProjectName string `yaml:"projectName" validate:"required"`
UserDomainName string `yaml:"userDomainName" validate:"required"`
ProjectDomainName string `yaml:"projectDomainName" validate:"required"`
RegionName string `yaml:"regionName" validate:"required"`
Username string `yaml:"username" validate:"required"`
Password string `yaml:"password"`
DirectDownload *bool `yaml:"directDownload" validate:"required"`
Measurements measurements.M `yaml:"measurements" validate:"required,no_placeholders"`
}
// QEMUConfig holds config information for QEMU based Constellation deployments.
type QEMUConfig struct {
ImageFormat string `yaml:"imageFormat" validate:"oneof=qcow2 raw"`
VCPUs int `yaml:"vcpus" validate:"required"`
Memory int `yaml:"memory" validate:"required"`
MetadataAPIImage string `yaml:"metadataAPIServer" validate:"required"`
LibvirtURI string `yaml:"libvirtSocket"`
LibvirtContainerImage string `yaml:"libvirtContainerImage"`
NVRAM string `yaml:"nvram" validate:"required"`
Firmware string `yaml:"firmware"`
Measurements measurements.M `yaml:"measurements" validate:"required,no_placeholders"`
}
// V2ToV3 converts an existing v2 config to a v3 config.
func V2ToV3(path string, fileHandler file.Handler) error {
// Read old format
var cfgV2 Config
if err := fileHandler.ReadYAML(path, &cfgV2); err != nil {
return fmt.Errorf("reading config file %s using v2 format: %w", path, err)
}
// Migrate to new format
var cfgV3 config.Config
cfgV3.Version = config.Version3
cfgV3.Image = cfgV2.Image
cfgV3.Name = cfgV2.Name
cfgV3.StateDiskSizeGB = cfgV2.StateDiskSizeGB
cfgV3.KubernetesVersion = cfgV2.KubernetesVersion
cfgV3.MicroserviceVersion = cfgV2.MicroserviceVersion
cfgV3.DebugCluster = cfgV2.DebugCluster
switch {
case cfgV2.Provider.AWS != nil:
cfgV3.Provider.AWS = &config.AWSConfig{
Region: cfgV2.Provider.AWS.Region,
Zone: cfgV2.Provider.AWS.Zone,
InstanceType: cfgV2.Provider.AWS.InstanceType,
StateDiskType: cfgV2.Provider.AWS.StateDiskType,
IAMProfileControlPlane: cfgV2.Provider.AWS.IAMProfileControlPlane,
IAMProfileWorkerNodes: cfgV2.Provider.AWS.IAMProfileWorkerNodes,
}
cfgV3.Attestation.AWSNitroTPM = &config.AWSNitroTPM{
Measurements: cfgV2.Provider.AWS.Measurements,
}
case cfgV2.Provider.Azure != nil:
cfgV3.Provider.Azure = &config.AzureConfig{
SubscriptionID: cfgV2.Provider.Azure.SubscriptionID,
TenantID: cfgV2.Provider.Azure.TenantID,
Location: cfgV2.Provider.Azure.Location,
ResourceGroup: cfgV2.Provider.Azure.ResourceGroup,
UserAssignedIdentity: cfgV2.Provider.Azure.UserAssignedIdentity,
AppClientID: cfgV2.Provider.Azure.AppClientID,
ClientSecretValue: cfgV2.Provider.Azure.ClientSecretValue,
InstanceType: cfgV2.Provider.Azure.InstanceType,
StateDiskType: cfgV2.Provider.Azure.StateDiskType,
DeployCSIDriver: cfgV2.Provider.Azure.DeployCSIDriver,
SecureBoot: cfgV2.Provider.Azure.SecureBoot,
}
// If an attestation war explicitly set to AzureTrustedLaunch,
// generate a config for AzureTrustedLaunch. Otherwise, generate one for AzureSEVSNP.
if attestVariant, err := variant.FromString(cfgV2.AttestationVariant); err == nil && attestVariant.Equal(variant.AzureTrustedLaunch{}) {
cfgV3.Attestation.AzureTrustedLaunch = &config.AzureTrustedLaunch{
Measurements: cfgV2.Provider.Azure.Measurements,
}
} else {
cfgV3.Attestation.AzureSEVSNP = config.DefaultForAzureSEVSNP()
cfgV3.Attestation.AzureSEVSNP.Measurements = cfgV2.Provider.Azure.Measurements
cfgV3.Attestation.AzureSEVSNP.FirmwareSignerConfig = config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: cfgV2.Provider.Azure.IDKeyDigest,
EnforcementPolicy: cfgV2.Provider.Azure.EnforceIDKeyDigest,
}
}
case cfgV2.Provider.GCP != nil:
cfgV3.Provider.GCP = &config.GCPConfig{
Project: cfgV2.Provider.GCP.Project,
Region: cfgV2.Provider.GCP.Region,
Zone: cfgV2.Provider.GCP.Zone,
ServiceAccountKeyPath: cfgV2.Provider.GCP.ServiceAccountKeyPath,
InstanceType: cfgV2.Provider.GCP.InstanceType,
StateDiskType: cfgV2.Provider.GCP.StateDiskType,
DeployCSIDriver: cfgV2.Provider.GCP.DeployCSIDriver,
}
cfgV3.Attestation.GCPSEVES = &config.GCPSEVES{
Measurements: cfgV2.Provider.GCP.Measurements,
}
case cfgV2.Provider.OpenStack != nil:
cfgV3.Provider.OpenStack = &config.OpenStackConfig{
Cloud: cfgV2.Provider.OpenStack.Cloud,
AvailabilityZone: cfgV2.Provider.OpenStack.AvailabilityZone,
FlavorID: cfgV2.Provider.OpenStack.FlavorID,
FloatingIPPoolID: cfgV2.Provider.OpenStack.FloatingIPPoolID,
AuthURL: cfgV2.Provider.OpenStack.AuthURL,
ProjectID: cfgV2.Provider.OpenStack.ProjectID,
ProjectName: cfgV2.Provider.OpenStack.ProjectName,
UserDomainName: cfgV2.Provider.OpenStack.UserDomainName,
ProjectDomainName: cfgV2.Provider.OpenStack.ProjectDomainName,
RegionName: cfgV2.Provider.OpenStack.RegionName,
Username: cfgV2.Provider.OpenStack.Username,
Password: cfgV2.Provider.OpenStack.Password,
DirectDownload: cfgV2.Provider.OpenStack.DirectDownload,
}
cfgV3.Attestation.QEMUVTPM = &config.QEMUVTPM{
Measurements: cfgV2.Provider.OpenStack.Measurements,
}
case cfgV2.Provider.QEMU != nil:
cfgV3.Provider.QEMU = &config.QEMUConfig{
ImageFormat: cfgV2.Provider.QEMU.ImageFormat,
VCPUs: cfgV2.Provider.QEMU.VCPUs,
Memory: cfgV2.Provider.QEMU.Memory,
MetadataAPIImage: cfgV2.Provider.QEMU.MetadataAPIImage,
LibvirtURI: cfgV2.Provider.QEMU.LibvirtURI,
LibvirtContainerImage: cfgV2.Provider.QEMU.LibvirtContainerImage,
NVRAM: cfgV2.Provider.QEMU.NVRAM,
Firmware: cfgV2.Provider.QEMU.Firmware,
}
cfgV3.Attestation.QEMUVTPM = &config.QEMUVTPM{
Measurements: cfgV2.Provider.QEMU.Measurements,
}
}
// Create backup
if err := os.Rename(path, path+".backup.v2"); err != nil {
return fmt.Errorf("creating backup: %w", err)
}
// Write migrated config
if err := fileHandler.WriteYAML(path, cfgV3, file.OptOverwrite); err != nil {
return fmt.Errorf("writing %s: %w", path, err)
}
return nil
}

View File

@ -104,11 +104,7 @@ func validateAWSInstanceType(fl validator.FieldLevel) bool {
}
func (c *Config) validateAzureInstanceType(fl validator.FieldLevel) bool {
attestVariant, err := variant.FromString(c.AttestationVariant)
if err != nil {
return false
}
acceptNonCVM := attestVariant.Equal(variant.AzureTrustedLaunch{})
acceptNonCVM := c.GetAttestationConfig().GetVariant().Equal(variant.AzureTrustedLaunch{})
return validInstanceTypeForProvider(fl.Field().String(), acceptNonCVM, cloudprovider.Azure)
}
@ -144,6 +140,71 @@ func validateProvider(sl validator.StructLevel) {
}
}
func validateAttestation(sl validator.StructLevel) {
attestation := sl.Current().Interface().(AttestationConfig)
attestationCount := 0
if attestation.AWSNitroTPM != nil {
attestationCount++
}
if attestation.AzureSEVSNP != nil {
attestationCount++
}
if attestation.AzureTrustedLaunch != nil {
attestationCount++
}
if attestation.GCPSEVES != nil {
attestationCount++
}
if attestation.QEMUVTPM != nil {
attestationCount++
}
if attestationCount < 1 {
sl.ReportError(attestation, "Attestation", "Attestation", "no_attestation", "")
} else if attestationCount > 1 {
sl.ReportError(attestation, "Attestation", "Attestation", "more_than_one_attestation", "")
}
}
func translateNoAttestationError(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("no_attestation", fe.Field())
return t
}
func registerNoAttestationError(ut ut.Translator) error {
return ut.Add("no_attestation", "{0}: No attestation has been defined (requires either AWSNitroTPM, AzureSEVSNP, AzureTrustedLaunch, GCPSEVES, or QEMUVTPM)", true)
}
func registerMoreThanOneAttestationError(ut ut.Translator) error {
return ut.Add("more_than_one_attestation", "{0}: Only one attestation can be defined ({1} are defined)", true)
}
func (c *Config) translateMoreThanOneAttestationError(ut ut.Translator, fe validator.FieldError) string {
definedAttestations := make([]string, 0)
if c.Attestation.AWSNitroTPM != nil {
definedAttestations = append(definedAttestations, "AWSNitroTPM")
}
if c.Attestation.AzureSEVSNP != nil {
definedAttestations = append(definedAttestations, "AzureSEVSNP")
}
if c.Attestation.AzureTrustedLaunch != nil {
definedAttestations = append(definedAttestations, "AzureTrustedLaunch")
}
if c.Attestation.GCPSEVES != nil {
definedAttestations = append(definedAttestations, "GCPSEVES")
}
if c.Attestation.QEMUVTPM != nil {
definedAttestations = append(definedAttestations, "QEMUVTPM")
}
t, _ := ut.T("more_than_one_attestation", fe.Field(), strings.Join(definedAttestations, ", "))
return t
}
func registerTranslateAWSInstanceTypeError(ut ut.Translator) error {
return ut.Add("aws_instance_type", fmt.Sprintf("{0} must be an instance from one of the following families types with size xlarge or higher: %v", instancetypes.AWSSupportedInstanceFamilies), true)
}
@ -285,10 +346,7 @@ func (c *Config) translateAzureInstanceTypeError(ut ut.Translator, fe validator.
// Suggest trusted launch VMs if confidential VMs have been specifically disabled
var t string
attestVariant, err := variant.FromString(c.AttestationVariant)
if err != nil {
return ""
}
attestVariant := c.GetAttestationConfig().GetVariant()
if attestVariant.Equal(variant.AzureTrustedLaunch{}) {
t, _ = ut.T("azure_instance_type", fe.Field(), fmt.Sprintf("%v", instancetypes.AzureTrustedLaunchInstanceTypes))
} else {
@ -299,7 +357,7 @@ func (c *Config) translateAzureInstanceTypeError(ut ut.Translator, fe validator.
}
func validateNoPlaceholder(fl validator.FieldLevel) bool {
return len(getPlaceholderEntries(fl.Field().Interface().(Measurements))) == 0
return len(getPlaceholderEntries(fl.Field().Interface().(measurements.M))) == 0
}
func registerContainsPlaceholderError(ut ut.Translator) error {
@ -307,7 +365,7 @@ func registerContainsPlaceholderError(ut ut.Translator) error {
}
func translateContainsPlaceholderError(ut ut.Translator, fe validator.FieldError) string {
placeholders := getPlaceholderEntries(fe.Value().(Measurements))
placeholders := getPlaceholderEntries(fe.Value().(measurements.M))
msg := fmt.Sprintf("Measurements %v contain", placeholders)
if len(placeholders) == 1 {
msg = fmt.Sprintf("Measurement %v contains", placeholders)
@ -317,7 +375,7 @@ func translateContainsPlaceholderError(ut ut.Translator, fe validator.FieldError
return t
}
func getPlaceholderEntries(m Measurements) []uint32 {
func getPlaceholderEntries(m measurements.M) []uint32 {
var placeholders []uint32
placeholder := measurements.PlaceHolderMeasurement()
@ -455,59 +513,6 @@ func (c *Config) validateName(fl validator.FieldLevel) bool {
return len(fl.Field().String()) <= constants.ConstellationNameLength
}
func registerValidAttestVariantError(ut ut.Translator) error {
return ut.Add("valid_attestation_variant", `"{0}" is not a valid attestation variant for CSP {1}`, true)
}
func (c *Config) translateValidAttestVariantError(ut ut.Translator, _ validator.FieldError) string {
csp := c.GetProvider()
t, _ := ut.T("valid_attestation_variant", c.AttestationVariant, csp.String())
return t
}
func (c *Config) validAttestVariant(_ validator.FieldLevel) bool {
// TODO(AB#3040): function will be obsolete in v2.8
// attestationVariant will be refactored into a required struct
c.addMissingVariant()
attestationVariant, err := variant.FromString(c.AttestationVariant)
if err != nil {
return false
}
// make sure the variant is valid for the chosen CSP
switch attestationVariant {
case variant.AWSNitroTPM{}:
return c.Provider.AWS != nil
case variant.AzureSEVSNP{}, variant.AzureTrustedLaunch{}:
return c.Provider.Azure != nil
case variant.GCPSEVES{}:
return c.Provider.GCP != nil
case variant.QEMUVTPM{}:
return c.Provider.QEMU != nil || c.Provider.OpenStack != nil
default:
return false
}
}
func (c *Config) addMissingVariant() {
if c.AttestationVariant != "" {
return
}
switch c.GetProvider() {
case cloudprovider.AWS:
c.AttestationVariant = variant.AWSNitroTPM{}.String()
case cloudprovider.Azure:
c.AttestationVariant = variant.AzureSEVSNP{}.String()
case cloudprovider.GCP:
c.AttestationVariant = variant.GCPSEVES{}.String()
case cloudprovider.QEMU:
c.AttestationVariant = variant.QEMUVTPM{}.String()
case cloudprovider.OpenStack:
c.AttestationVariant = variant.QEMUVTPM{}.String()
}
}
func warnDeprecated(fl validator.FieldLevel) bool {
fmt.Fprintf(
os.Stderr,

View File

@ -106,18 +106,12 @@ const (
// ServiceBasePath is the base path for the mounted micro service's files.
ServiceBasePath = "/var/config"
// MeasurementsFilename is the filename of CC measurements.
MeasurementsFilename = "measurements"
// AttestationConfigFilename is the filename of the config used for CC validation.
AttestationConfigFilename = "attestationConfig"
// MeasurementSaltFilename is the filename of the salt used in creation of the clusterID.
MeasurementSaltFilename = "measurementSalt"
// MeasurementSecretFilename is the filename of the secret used in creation of the clusterID.
MeasurementSecretFilename = "measurementSecret"
// IDKeyDigestFilename is the name of the file holding the currently enforced idkeydigest.
IDKeyDigestFilename = "idkeydigests"
// EnforceIDKeyDigestFilename is the name of the file configuring whether idkeydigest is enforced or not.
EnforceIDKeyDigestFilename = "enforceIdKeyDigest"
// IDKeyConfigFilename is the name of the file holding the configuration for validating the SEV-SNP ID key digest.
IDKeyConfigFilename = "idKeyConfig"
// K8sVersionFieldName is the name of the of the key holding the wanted Kubernetes version.
K8sVersionFieldName = "cluster-version"

View File

@ -120,7 +120,7 @@ func (h *Handler) WriteJSON(name string, content any, options ...Option) error {
}
// ReadYAML reads a YAML file from name and unmarshals it into the content interface.
// The interface content must be a pointer to a YAML marchalable object.
// The interface content must be a pointer to a YAML marshalable object.
func (h *Handler) ReadYAML(name string, content any) error {
return h.readYAML(name, content, false)
}

View File

@ -199,6 +199,12 @@ func (k *Kubectl) AddNodeSelectorsToDeployment(ctx context.Context, selectors ma
return nil
}
// DeleteStorageClass deletes the storage class with the given name.
// TODO: Remove with v2.9.
func (k *Kubectl) DeleteStorageClass(ctx context.Context, name string) error {
return k.StorageV1().StorageClasses().Delete(ctx, name, metav1.DeleteOptions{})
}
// parseCRD takes a byte slice of data and tries to create a CustomResourceDefinition object from it.
func parseCRD(crdString []byte) (*v1.CustomResourceDefinition, error) {
sch := runtime.NewScheme()

View File

@ -12,15 +12,12 @@ go_library(
deps = [
"//internal/atls",
"//internal/attestation/choose",
"//internal/attestation/idkeydigest",
"//internal/attestation/measurements",
"//internal/config",
"//internal/constants",
"//internal/file",
"//internal/logger",
"//internal/variant",
"@com_github_fsnotify_fsnotify//:fsnotify",
"@com_github_spf13_afero//:afero",
"@org_uber_go_zap//:zap",
],
)
@ -34,7 +31,6 @@ go_test(
embed = [":watcher"],
deps = [
"//internal/atls",
"//internal/attestation/idkeydigest",
"//internal/attestation/measurements",
"//internal/config",
"//internal/constants",

View File

@ -9,22 +9,17 @@ package watcher
import (
"context"
"encoding/asn1"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"sync"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/choose"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/spf13/afero"
)
// Updatable implements an updatable atls.Validator.
@ -71,54 +66,17 @@ func (u *Updatable) Update() error {
u.log.Infof("Updating expected measurements")
var measurements measurements.M
if err := u.fileHandler.ReadJSON(filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename), &measurements); err != nil {
return err
}
u.log.Debugf("New measurements: %+v", measurements)
// Read ID Key config
var idKeyCfg config.SNPFirmwareSignerConfig
if u.variant.Equal(variant.AzureSEVSNP{}) {
u.log.Infof("Updating SEV-SNP ID Key config")
err := u.fileHandler.ReadJSON(filepath.Join(constants.ServiceBasePath, constants.IDKeyConfigFilename), &idKeyCfg)
if err != nil {
if !errors.Is(err, afero.ErrFileNotFound) {
return fmt.Errorf("reading ID Key config: %w", err)
}
u.log.Warnf("ID Key config file not found, falling back to old format (v2.6 or earlier)")
// v2.6 fallback
// TODO: Remove after v2.7 release
var digest idkeydigest.List
var enforceIDKeyDigest idkeydigest.Enforcement
enforceRaw, err := u.fileHandler.Read(filepath.Join(constants.ServiceBasePath, constants.EnforceIDKeyDigestFilename))
data, err := u.fileHandler.Read(filepath.Join(constants.ServiceBasePath, constants.AttestationConfigFilename))
if err != nil {
return err
}
enforceIDKeyDigest = idkeydigest.EnforcePolicyFromString(string(enforceRaw))
cfg, err := config.UnmarshalAttestationConfig(data, u.variant)
if err != nil {
return fmt.Errorf("parsing content of EnforceIdKeyDigestFilename: %s: %w", enforceRaw, err)
return fmt.Errorf("unmarshaling config: %w", err)
}
u.log.Debugf("New expected measurements: %+v", cfg.GetMeasurements())
idkeydigestRaw, err := u.fileHandler.Read(filepath.Join(constants.ServiceBasePath, constants.IDKeyDigestFilename))
if err != nil {
return err
}
if err = json.Unmarshal(idkeydigestRaw, &digest); err != nil {
return fmt.Errorf("unmarshaling content of IDKeyDigestFilename: %s: %w", idkeydigestRaw, err)
}
idKeyCfg.AcceptedKeyDigests = digest
idKeyCfg.EnforcementPolicy = enforceIDKeyDigest
}
u.log.Debugf("New ID Key config: %+v", idKeyCfg)
}
validator, err := choose.Validator(u.variant, measurements, idKeyCfg, u.log)
validator, err := choose.Validator(cfg, u.log)
if err != nil {
return fmt.Errorf("updating validator: %w", err)
}

View File

@ -18,7 +18,6 @@ import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -41,29 +40,28 @@ func TestMain(m *testing.M) {
func TestNewUpdateableValidator(t *testing.T) {
testCases := map[string]struct {
variant variant.Variant
writeFile bool
config config.AttestationCfg
wantErr bool
}{
"azure": {
variant: variant.AzureSEVSNP{},
writeFile: true,
config: config.DefaultForAzureSEVSNP(),
},
"gcp": {
variant: variant.GCPSEVES{},
writeFile: true,
config: &config.GCPSEVES{Measurements: measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)}},
},
"qemu": {
variant: variant.QEMUVTPM{},
writeFile: true,
config: &config.QEMUVTPM{Measurements: measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)}},
},
"no file": {
variant: variant.AzureSEVSNP{},
writeFile: false,
wantErr: true,
},
"invalid provider": {
variant: fakeOID{1, 3, 9900, 9999, 9999},
writeFile: true,
config: &config.GCPSEVES{Measurements: measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)}},
wantErr: true,
},
}
@ -74,18 +72,10 @@ func TestNewUpdateableValidator(t *testing.T) {
require := require.New(t)
handler := file.NewHandler(afero.NewMemMapFs())
if tc.writeFile {
if tc.config != nil {
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename),
measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)},
))
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.IDKeyConfigFilename),
config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: idkeydigest.DefaultList(),
EnforcementPolicy: idkeydigest.WarnOnly,
},
filepath.Join(constants.ServiceBasePath, constants.AttestationConfigFilename),
tc.config,
))
}
@ -121,15 +111,8 @@ func TestUpdate(t *testing.T) {
// write measurement config
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename),
measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)},
))
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.IDKeyConfigFilename),
config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: idkeydigest.List{[]byte{0x00}},
EnforcementPolicy: idkeydigest.WarnOnly,
},
filepath.Join(constants.ServiceBasePath, constants.AttestationConfigFilename),
&config.DummyCfg{Measurements: measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)}},
))
// call update once to initialize the server's validator
@ -164,18 +147,6 @@ func TestUpdate(t *testing.T) {
defer resp.Body.Close()
}
assert.Error(err)
// test old ID Key Digest format
require.NoError(handler.Write(
filepath.Join(constants.ServiceBasePath, constants.IDKeyDigestFilename),
[]byte{},
))
require.NoError(handler.Write(
filepath.Join(constants.ServiceBasePath, constants.EnforceIDKeyDigestFilename),
[]byte("false"),
))
assert.NoError(validator.Update())
}
func TestOIDConcurrency(t *testing.T) {
@ -184,15 +155,8 @@ func TestOIDConcurrency(t *testing.T) {
handler := file.NewHandler(afero.NewMemMapFs())
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename),
measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)},
))
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.IDKeyConfigFilename),
config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: idkeydigest.List{[]byte{0x00}},
EnforcementPolicy: idkeydigest.WarnOnly,
},
filepath.Join(constants.ServiceBasePath, constants.AttestationConfigFilename),
&config.DummyCfg{Measurements: measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)}},
))
// create server
@ -231,17 +195,10 @@ func TestUpdateConcurrency(t *testing.T) {
variant: variant.Dummy{},
}
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename),
measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)},
filepath.Join(constants.ServiceBasePath, constants.AttestationConfigFilename),
&config.DummyCfg{Measurements: measurements.M{11: measurements.WithAllBytes(0x00, measurements.Enforce)}},
file.OptNone,
))
require.NoError(handler.WriteJSON(
filepath.Join(constants.ServiceBasePath, constants.IDKeyConfigFilename),
config.SNPFirmwareSignerConfig{
AcceptedKeyDigests: idkeydigest.List{[]byte{0x00}},
EnforcementPolicy: idkeydigest.WarnOnly,
},
))
var wg sync.WaitGroup

View File

@ -104,8 +104,8 @@ func main() {
defer watcher.Close()
go func() {
log.Infof("starting file watcher for measurements file %s", filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename))
if err := watcher.Watch(filepath.Join(constants.ServiceBasePath, constants.MeasurementsFilename)); err != nil {
log.Infof("starting file watcher for measurements file %s", filepath.Join(constants.ServiceBasePath, constants.AttestationConfigFilename))
if err := watcher.Watch(filepath.Join(constants.ServiceBasePath, constants.AttestationConfigFilename)); err != nil {
log.With(zap.Error(err)).Fatalf("Failed to watch measurements file")
}
}()