From 4228981a82340e6e39438bb18076e497bcbee772 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:31:41 +0200 Subject: [PATCH] config: add SEV-SNP config options for GCP --- internal/config/BUILD.bazel | 1 + internal/config/attestation.go | 2 + internal/config/attestation_test.go | 3 + internal/config/config.go | 57 ++++++++----- internal/config/config_doc.go | 75 ++++++++++++++-- internal/config/config_test.go | 7 +- internal/config/gcp.go | 127 ++++++++++++++++++++++++++++ internal/config/validation.go | 8 +- 8 files changed, 247 insertions(+), 33 deletions(-) create mode 100644 internal/config/gcp.go diff --git a/internal/config/BUILD.bazel b/internal/config/BUILD.bazel index c653c489c..8ea071ae8 100644 --- a/internal/config/BUILD.bazel +++ b/internal/config/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "azure.go", "config.go", "config_doc.go", + "gcp.go", # keep "image_enterprise.go", # keep diff --git a/internal/config/attestation.go b/internal/config/attestation.go index dc4d8fb83..f635ebbbd 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -52,6 +52,8 @@ func UnmarshalAttestationConfig(data []byte, attestVariant variant.Variant) (Att return unmarshalTypedConfig[*AzureTDX](data) case variant.GCPSEVES{}: return unmarshalTypedConfig[*GCPSEVES](data) + case variant.GCPSEVSNP{}: + return unmarshalTypedConfig[*GCPSEVSNP](data) case variant.QEMUVTPM{}: return unmarshalTypedConfig[*QEMUVTPM](data) case variant.QEMUTDX{}: diff --git a/internal/config/attestation_test.go b/internal/config/attestation_test.go index e0e3492dc..a690ba40b 100644 --- a/internal/config/attestation_test.go +++ b/internal/config/attestation_test.go @@ -41,6 +41,9 @@ func TestUnmarshalAttestationConfig(t *testing.T) { "GCPSEVES": { cfg: &GCPSEVES{Measurements: measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVES{})}, }, + "GCPSEVSNP": { + cfg: DefaultForGCPSEVSNP(), + }, "QEMUVTPM": { cfg: &QEMUVTPM{Measurements: measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUVTPM{})}, }, diff --git a/internal/config/config.go b/internal/config/config.go index 10ac013d1..c7a7ec63c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -278,6 +278,9 @@ type AttestationConfig struct { // GCP SEV-ES attestation. GCPSEVES *GCPSEVES `yaml:"gcpSEVES,omitempty" validate:"omitempty,dive"` // description: | + // GCP SEV-SNP attestation. + GCPSEVSNP *GCPSEVSNP `yaml:"gcpSEVSNP,omitempty" validate:"omitempty,dive"` + // description: | // QEMU tdx attestation. QEMUTDX *QEMUTDX `yaml:"qemuTDX,omitempty" validate:"omitempty,dive"` // description: | @@ -390,6 +393,7 @@ func Default() *Config { AzureTDX: DefaultForAzureTDX(), AzureTrustedLaunch: &AzureTrustedLaunch{Measurements: measurements.DefaultsFor(cloudprovider.Azure, variant.AzureTrustedLaunch{})}, GCPSEVES: &GCPSEVES{Measurements: measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVES{})}, + GCPSEVSNP: DefaultForGCPSEVSNP(), QEMUVTPM: &QEMUVTPM{Measurements: measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUVTPM{})}, }, } @@ -518,6 +522,9 @@ func (c *Config) UpdateMeasurements(newMeasurements measurements.M) { if c.Attestation.GCPSEVES != nil { c.Attestation.GCPSEVES.Measurements.CopyFrom(newMeasurements) } + if c.Attestation.GCPSEVSNP != nil { + c.Attestation.GCPSEVSNP.Measurements.CopyFrom(newMeasurements) + } if c.Attestation.QEMUVTPM != nil { c.Attestation.QEMUVTPM.Measurements.CopyFrom(newMeasurements) } @@ -570,6 +577,8 @@ func (c *Config) SetAttestation(attestation variant.Variant) { c.Attestation = AttestationConfig{AzureTrustedLaunch: currentAttestationConfigs.AzureTrustedLaunch} case variant.GCPSEVES: c.Attestation = AttestationConfig{GCPSEVES: currentAttestationConfigs.GCPSEVES} + case variant.GCPSEVSNP: + c.Attestation = AttestationConfig{GCPSEVSNP: currentAttestationConfigs.GCPSEVSNP} case variant.QEMUVTPM: c.Attestation = AttestationConfig{QEMUVTPM: currentAttestationConfigs.QEMUVTPM} } @@ -637,6 +646,9 @@ func (c *Config) GetAttestationConfig() AttestationCfg { if c.Attestation.GCPSEVES != nil { return c.Attestation.GCPSEVES } + if c.Attestation.GCPSEVSNP != nil { + return c.Attestation.GCPSEVSNP + } if c.Attestation.QEMUVTPM != nil { return c.Attestation.QEMUVTPM } @@ -964,28 +976,29 @@ type GCPSEVES struct { Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` } -// GetVariant returns gcp-sev-es as the variant. -func (GCPSEVES) GetVariant() variant.Variant { - return variant.GCPSEVES{} -} - -// GetMeasurements returns the measurements used for attestation. -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 +// GCPSEVSNP is the configuration for GCP SEV-SNP attestation. +type GCPSEVSNP struct { + // description: | + // Expected TPM measurements. + Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` + // description: | + // Lowest acceptable bootloader version. + BootloaderVersion AttestationVersion `json:"bootloaderVersion" yaml:"bootloaderVersion"` + // description: | + // Lowest acceptable TEE version. + TEEVersion AttestationVersion `json:"teeVersion" yaml:"teeVersion"` + // description: | + // Lowest acceptable SEV-SNP version. + SNPVersion AttestationVersion `json:"snpVersion" yaml:"snpVersion"` + // description: | + // Lowest acceptable microcode version. + MicrocodeVersion AttestationVersion `json:"microcodeVersion" yaml:"microcodeVersion"` + // description: | + // AMD Root Key certificate used to verify the SEV-SNP certificate chain. + AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"` + // description: | + // AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate. + AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty"` } // QEMUVTPM is the configuration for QEMU vTPM attestation. diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index 2168b7f98..56f358d03 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -23,6 +23,7 @@ var ( UnsupportedAppRegistrationErrorDoc encoder.Doc SNPFirmwareSignerConfigDoc encoder.Doc GCPSEVESDoc encoder.Doc + GCPSEVSNPDoc encoder.Doc QEMUVTPMDoc encoder.Doc QEMUTDXDoc encoder.Doc AWSSEVSNPDoc encoder.Doc @@ -388,7 +389,7 @@ func init() { FieldName: "attestation", }, } - AttestationConfigDoc.Fields = make([]encoder.Doc, 8) + AttestationConfigDoc.Fields = make([]encoder.Doc, 9) AttestationConfigDoc.Fields[0].Name = "awsSEVSNP" AttestationConfigDoc.Fields[0].Type = "AWSSEVSNP" AttestationConfigDoc.Fields[0].Note = "" @@ -419,16 +420,21 @@ func init() { AttestationConfigDoc.Fields[5].Note = "" AttestationConfigDoc.Fields[5].Description = "GCP SEV-ES attestation." AttestationConfigDoc.Fields[5].Comments[encoder.LineComment] = "GCP SEV-ES attestation." - AttestationConfigDoc.Fields[6].Name = "qemuTDX" - AttestationConfigDoc.Fields[6].Type = "QEMUTDX" + AttestationConfigDoc.Fields[6].Name = "gcpSEVSNP" + AttestationConfigDoc.Fields[6].Type = "GCPSEVSNP" AttestationConfigDoc.Fields[6].Note = "" - AttestationConfigDoc.Fields[6].Description = "QEMU tdx attestation." - AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "QEMU tdx attestation." - AttestationConfigDoc.Fields[7].Name = "qemuVTPM" - AttestationConfigDoc.Fields[7].Type = "QEMUVTPM" + AttestationConfigDoc.Fields[6].Description = "description: |\n GCP SEV-SNP attestation.\n" + AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "description: |" + AttestationConfigDoc.Fields[7].Name = "qemuTDX" + AttestationConfigDoc.Fields[7].Type = "QEMUTDX" AttestationConfigDoc.Fields[7].Note = "" - AttestationConfigDoc.Fields[7].Description = "QEMU vTPM attestation." - AttestationConfigDoc.Fields[7].Comments[encoder.LineComment] = "QEMU vTPM attestation." + AttestationConfigDoc.Fields[7].Description = "QEMU tdx attestation." + AttestationConfigDoc.Fields[7].Comments[encoder.LineComment] = "QEMU tdx attestation." + AttestationConfigDoc.Fields[8].Name = "qemuVTPM" + AttestationConfigDoc.Fields[8].Type = "QEMUVTPM" + AttestationConfigDoc.Fields[8].Note = "" + AttestationConfigDoc.Fields[8].Description = "QEMU vTPM attestation." + AttestationConfigDoc.Fields[8].Comments[encoder.LineComment] = "QEMU vTPM attestation." NodeGroupDoc.Type = "NodeGroup" NodeGroupDoc.Comments[encoder.LineComment] = "NodeGroup defines a group of nodes with the same role and configuration." @@ -518,6 +524,52 @@ func init() { GCPSEVESDoc.Fields[0].Description = "Expected TPM measurements." GCPSEVESDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + GCPSEVSNPDoc.Type = "GCPSEVSNP" + GCPSEVSNPDoc.Comments[encoder.LineComment] = "GCPSEVSNP is the configuration for GCP SEV-SNP attestation." + GCPSEVSNPDoc.Description = "GCPSEVSNP is the configuration for GCP SEV-SNP attestation." + GCPSEVSNPDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "AttestationConfig", + FieldName: "gcpSEVSNP", + }, + } + GCPSEVSNPDoc.Fields = make([]encoder.Doc, 7) + GCPSEVSNPDoc.Fields[0].Name = "measurements" + GCPSEVSNPDoc.Fields[0].Type = "M" + GCPSEVSNPDoc.Fields[0].Note = "" + GCPSEVSNPDoc.Fields[0].Description = "Expected TPM measurements." + GCPSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + GCPSEVSNPDoc.Fields[1].Name = "bootloaderVersion" + GCPSEVSNPDoc.Fields[1].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[1].Note = "" + GCPSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version." + GCPSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version." + GCPSEVSNPDoc.Fields[2].Name = "teeVersion" + GCPSEVSNPDoc.Fields[2].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[2].Note = "" + GCPSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version." + GCPSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version." + GCPSEVSNPDoc.Fields[3].Name = "snpVersion" + GCPSEVSNPDoc.Fields[3].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[3].Note = "" + GCPSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version." + GCPSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version." + GCPSEVSNPDoc.Fields[4].Name = "microcodeVersion" + GCPSEVSNPDoc.Fields[4].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[4].Note = "" + GCPSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version." + GCPSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version." + GCPSEVSNPDoc.Fields[5].Name = "amdRootKey" + GCPSEVSNPDoc.Fields[5].Type = "Certificate" + GCPSEVSNPDoc.Fields[5].Note = "" + GCPSEVSNPDoc.Fields[5].Description = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + GCPSEVSNPDoc.Fields[5].Comments[encoder.LineComment] = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + GCPSEVSNPDoc.Fields[6].Name = "amdSigningKey" + GCPSEVSNPDoc.Fields[6].Type = "Certificate" + GCPSEVSNPDoc.Fields[6].Note = "" + GCPSEVSNPDoc.Fields[6].Description = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + GCPSEVSNPDoc.Fields[6].Comments[encoder.LineComment] = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + 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." @@ -779,6 +831,10 @@ func (_ GCPSEVES) Doc() *encoder.Doc { return &GCPSEVESDoc } +func (_ GCPSEVSNP) Doc() *encoder.Doc { + return &GCPSEVSNPDoc +} + func (_ QEMUVTPM) Doc() *encoder.Doc { return &QEMUVTPMDoc } @@ -825,6 +881,7 @@ func GetConfigurationDoc() *encoder.FileDoc { &UnsupportedAppRegistrationErrorDoc, &SNPFirmwareSignerConfigDoc, &GCPSEVESDoc, + &GCPSEVSNPDoc, &QEMUVTPMDoc, &QEMUTDXDoc, &AWSSEVSNPDoc, diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 013c50edc..fa9c0c2d0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -328,7 +328,7 @@ func TestFromFile(t *testing.T) { } func TestValidate(t *testing.T) { - const defaultErrCount = 32 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default + 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 azErrCount = 7 const awsErrCount = 8 const gcpErrCount = 8 @@ -735,6 +735,11 @@ func TestValidInstanceTypeForProvider(t *testing.T) { instanceTypes: instancetypes.GCPInstanceTypes, expectedResult: true, }, + "gcp sev-snp": { + variant: variant.GCPSEVSNP{}, + instanceTypes: instancetypes.GCPInstanceTypes, + expectedResult: true, + }, "put gcp when azure is set": { variant: variant.AzureSEVSNP{}, instanceTypes: instancetypes.GCPInstanceTypes, diff --git a/internal/config/gcp.go b/internal/config/gcp.go new file mode 100644 index 000000000..299af595f --- /dev/null +++ b/internal/config/gcp.go @@ -0,0 +1,127 @@ +/* +Copyright (c) Edgeless Systems GmbH +SPDX-License-Identifier: AGPL-3.0-only +*/ +package config + +import ( + "bytes" + "context" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" +) + +var _ svnResolveMarshaller = &GCPSEVSNP{} + +// DefaultForGCPSEVSNP provides a valid default configuration for GCP SEV-SNP attestation. +func DefaultForGCPSEVSNP() *GCPSEVSNP { + return &GCPSEVSNP{ + Measurements: measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVSNP{}), + BootloaderVersion: NewLatestPlaceholderVersion(), + TEEVersion: NewLatestPlaceholderVersion(), + SNPVersion: NewLatestPlaceholderVersion(), + MicrocodeVersion: NewLatestPlaceholderVersion(), + AMDRootKey: mustParsePEM(arkPEM), + } +} + +// GetVariant returns gcp-sev-snp as the variant. +func (GCPSEVSNP) GetVariant() variant.Variant { + return variant.GCPSEVSNP{} +} + +// GetMeasurements returns the measurements used for attestation. +func (c GCPSEVSNP) GetMeasurements() measurements.M { + return c.Measurements +} + +// SetMeasurements updates a config's measurements using the given measurements. +func (c *GCPSEVSNP) SetMeasurements(m measurements.M) { + c.Measurements = m +} + +// EqualTo returns true if the config is equal to the given config. +func (c GCPSEVSNP) EqualTo(other AttestationCfg) (bool, error) { + otherCfg, ok := other.(*GCPSEVSNP) + if !ok { + return false, fmt.Errorf("cannot compare %T with %T", c, other) + } + + 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) + signingKeyEqual := bytes.Equal(c.AMDSigningKey.Raw, otherCfg.AMDSigningKey.Raw) + + return measurementsEqual && bootloaderEqual && teeEqual && snpEqual && microcodeEqual && rootKeyEqual && signingKeyEqual, nil +} + +func (c *GCPSEVSNP) getToMarshallLatestWithResolvedVersions() AttestationCfg { + cp := *c + cp.BootloaderVersion.WantLatest = false + cp.TEEVersion.WantLatest = false + cp.SNPVersion.WantLatest = false + cp.MicrocodeVersion.WantLatest = false + return &cp +} + +// FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. +func (c *GCPSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error { + // Only talk to the API if at least one version number is set to latest. + if !(c.BootloaderVersion.WantLatest || c.TEEVersion.WantLatest || c.SNPVersion.WantLatest || c.MicrocodeVersion.WantLatest) { + return nil + } + + versions, err := fetcher.FetchSEVSNPVersionLatest(ctx, variant.GCPSEVSNP{}) + if err != nil { + return fmt.Errorf("fetching latest TCB versions from configapi: %w", err) + } + // set number and keep isLatest flag + c.mergeWithLatestVersion(versions.SEVSNPVersion) + return nil +} + +func (c *GCPSEVSNP) mergeWithLatestVersion(latest attestationconfigapi.SEVSNPVersion) { + if c.BootloaderVersion.WantLatest { + c.BootloaderVersion.Value = latest.Bootloader + } + if c.TEEVersion.WantLatest { + c.TEEVersion.Value = latest.TEE + } + if c.SNPVersion.WantLatest { + c.SNPVersion.Value = latest.SNP + } + if c.MicrocodeVersion.WantLatest { + c.MicrocodeVersion.Value = latest.Microcode + } +} + +// GetVariant returns gcp-sev-es as the variant. +func (GCPSEVES) GetVariant() variant.Variant { + return variant.GCPSEVES{} +} + +// GetMeasurements returns the measurements used for attestation. +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 +} diff --git a/internal/config/validation.go b/internal/config/validation.go index 5e0ef59ee..fab69ff29 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -202,6 +202,9 @@ func validateAttestation(sl validator.StructLevel) { if attestation.GCPSEVES != nil { attestationCount++ } + if attestation.GCPSEVSNP != nil { + attestationCount++ + } if attestation.QEMUVTPM != nil { attestationCount++ } @@ -364,6 +367,9 @@ func (c *Config) translateMoreThanOneAttestationError(ut ut.Translator, fe valid if c.Attestation.GCPSEVES != nil { definedAttestations = append(definedAttestations, "GCPSEVES") } + if c.Attestation.GCPSEVSNP != nil { + definedAttestations = append(definedAttestations, "GCPSEVSNP") + } if c.Attestation.QEMUVTPM != nil { definedAttestations = append(definedAttestations, "QEMUVTPM") } @@ -536,7 +542,7 @@ func validInstanceTypeForProvider(insType string, attestation variant.Variant) b return true } } - case variant.GCPSEVES{}: + case variant.GCPSEVES{}, variant.GCPSEVSNP{}: for _, instanceType := range instancetypes.GCPInstanceTypes { if insType == instanceType { return true