From 667e04193d8a1dcee7079ce4b7d870927f50170b Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:30:35 +0200 Subject: [PATCH 01/30] terraform: enable creation of SEV-SNP VMs on GCP --- .../infrastructure/gcp/.terraform.lock.hcl | 60 +++++++++++++------ terraform/infrastructure/gcp/main.tf | 14 ++++- .../gcp/modules/instance_group/main.tf | 16 ++++- .../gcp/modules/instance_group/variables.tf | 9 +++ .../modules/internal_load_balancer/main.tf | 2 +- .../gcp/modules/jump_host/main.tf | 2 +- .../gcp/modules/loadbalancer/main.tf | 2 +- terraform/infrastructure/gcp/variables.tf | 9 +++ 8 files changed, 91 insertions(+), 23 deletions(-) diff --git a/terraform/infrastructure/gcp/.terraform.lock.hcl b/terraform/infrastructure/gcp/.terraform.lock.hcl index bc58c4246c..5579933811 100644 --- a/terraform/infrastructure/gcp/.terraform.lock.hcl +++ b/terraform/infrastructure/gcp/.terraform.lock.hcl @@ -2,26 +2,50 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { - version = "5.17.0" - constraints = "5.17.0" + version = "5.23.0" + constraints = "5.23.0" hashes = [ - "h1:9DKCaGp9EFKDLWIOWI3yA/RgWTMh0EMD6+iggVXC9l0=", - "h1:JEfDiodirnMqwNaub/anXoOtWt68aEN80QtPJxg3jsc=", - "h1:TANQI64JuScQ2LTITQqz7eh1RjhYDItdbI5p1aBOtXY=", - "h1:dT3UftIyARC7YjS4yurPlNS7WJAHICDHMXSluAAvavA=", - "h1:lu84RYioCT4OxXbFBdqom4QvSPAjMkEyHPSIAxuS7oo=", - "zh:31b4d485ee66e6ff2eb1d8e476e694904447ce2b7143a2e067e4b80a84958d13", - "zh:32e86a51c4b0b29b7a18dd95616ea2976f08a4a7385e00f2bcab266217ee4320", - "zh:357f352bf04e7bc10d61d49296bf6503f31a3db0500169cb532afde7d318643e", - "zh:4b4637ca397cc771136edf7ec5578b5ab8631a8955a86d4fce3b8c40ca8c26b4", - "zh:4fe198b7427f7bf04270a5491a0352379c2b0a1caf12e206e6e224ceb085f56a", - "zh:7abb8509a61602d5ed4c801e7cd7c8299d109bc07980352251ba79880a99abab", - "zh:b1550fe08c650d8419860da1568d3f77093d269f880cad7d720d843b2a9ec545", - "zh:c91d7079646a3fdbb927085e368a16b221a23c17cf7455d5088f0c8f5da48c9f", - "zh:d367213a5f392852ef0708283df583703b2efd0b44f9e599cd055086c371cf74", - "zh:d5b557f294f4094a865afaa0611dc2e657d485b60903f12795eeedc2e1c3aa87", + "h1:2VJTKCZWQ1DaNwclFxSo27avsYwWgq/itwLZ3xKyl/o=", + "h1:4evtipODvV5s86gihS+jyk1cSW1xLn22jy8Ox8zzhAs=", + "h1:BD+iQfFcZ0OeaZI2JWDp2sLqSr+DfZtWy4yo1OVMnTI=", + "h1:my3kqg4hIpWLu2WwRewOFxBS+FXfkAIiw8xTYVPNS9M=", + "h1:xpm8QPNp2soGqIEnf4SNoZaTlQ/SbNH63BooJkSbgX0=", + "zh:18eaaa51a8b30fed61c73799b8716a9bd08ccd382bc395c63e45b9a52ed8b300", + "zh:20c71acf091a282db88473ec6f0a684ac59891713c49b2ff1cb35c1539da3121", + "zh:2e3e9ae1d3b045dcaa39053f4d1d066fa17e5b81f4ed7a5e57cc4e6e1e651900", + "zh:531d1552f251c5a0176543defa95c2cc259fc8b9359ef6fd3df404dcead555a0", + "zh:67a7800023fa09a7d87ac02231364988749663e37e2906aa89c70eecc5955ccf", + "zh:6a8076b59d2766a05ffe521cc115f3e8df7cd2ee4c6d60de4ee4636f47714f2e", + "zh:7b39fe720bb7a1f35cd0e4dfeff617338342fc2d16bb22274b42c080ff633140", + "zh:b181e04c32aa53ad78eaf6f2746ec5fd94977187ba7314ae8e9815ef6ea56532", + "zh:bf605be2f8942d5cabb8755ff0d18f243b53f1148f5f32db762667cf64bfa949", + "zh:e981988558310df5d94e56adaa76f7444d991357fe9600c46eb70fa61f4a1394", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f663776d79e7e5d131b4fbd68c152f2bef3e899a19c9baabe3a441e3f5e809ea", + ] +} + +provider "registry.terraform.io/hashicorp/google-beta" { + version = "5.23.0" + constraints = "5.23.0" + hashes = [ + "h1:EGIz78npj995XQdJyKRqgiCqFrcfDPXJwVrVw3PFGE0=", + "h1:cxF5B8zWRmTStRAY5o+A3iIFtsiKN0NNr72YTtKSSJw=", + "h1:irFKUONsaAiMFJPCyViRAuIWH/aRUKjEzL5mwzSMMRY=", + "h1:kiwwYe7qrzmxT5L/T6kuWMSqSR5THlGybmZ17hxpPI4=", + "h1:lvEvKrY8nPjumNwHxRmSXxmWnlq5bLq2CUq4FrUQDdM=", + "zh:074f276975ffc873d8f9848d54073ef8320428828611d803c82b7c2559c696fb", + "zh:12bc0f45071b1af5d4c2beddd1ad54c3d91f246c04a41d51570fed2f56d4e7f2", + "zh:2310eac5e8a0286d11a830f33b9d7b93804a02abb63874d8ff9f08b11cc015ed", + "zh:43d70d5a760afd0b4d7d21a852ea4b507c6a6673a2ecd135b6991097bae723ce", + "zh:44d0fb42b80504497c0983f34135c7619a7f7dcd22ed7ef3c916c4d444ee73d5", + "zh:663d82298c96decffc9617183d3d1d5b36fa4aa3e7922897cbed2ca7766c7609", + "zh:9b81cc5347409b8f99fbc5ac289e0f2c82a4904615919001555303621791729f", + "zh:bc532772de1286cc931b6f672044f71d6be66a9ea81961c38b544495c9d6d765", + "zh:c6d1c975bc55a1bd3729daa5bbb7153ae664e2086ed1acf8781581f547b1dce9", + "zh:caaa3ebbdcc74205622f3cd3544860989295fba63a62c1e74f5f5161bdf81d53", + "zh:e71df7cf923bf5a8b11ddce562266904505d5dd3eb25d3797bdb308940ad5890", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:fdad54c5e50751cef3f39a8666ff6adbb3bd860d396d5a9a0a3526e204f60454", ] } diff --git a/terraform/infrastructure/gcp/main.tf b/terraform/infrastructure/gcp/main.tf index f381955227..33c359b68b 100644 --- a/terraform/infrastructure/gcp/main.tf +++ b/terraform/infrastructure/gcp/main.tf @@ -2,7 +2,12 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" + } + + google-beta = { + source = "hashicorp/google-beta" + version = "5.23.0" } random = { @@ -18,6 +23,12 @@ provider "google" { zone = var.zone } +provider "google-beta" { + project = var.project + region = var.region + zone = var.zone +} + locals { uid = random_id.uid.hex name = "${var.name}-${local.uid}" @@ -175,6 +186,7 @@ module "instance_group" { labels = local.labels init_secret_hash = local.init_secret_hash custom_endpoint = var.custom_endpoint + cc_technology = var.cc_technology } resource "google_compute_address" "loadbalancer_ip_internal" { diff --git a/terraform/infrastructure/gcp/modules/instance_group/main.tf b/terraform/infrastructure/gcp/modules/instance_group/main.tf index 2681c4d47c..fe9da14ae7 100644 --- a/terraform/infrastructure/gcp/modules/instance_group/main.tf +++ b/terraform/infrastructure/gcp/modules/instance_group/main.tf @@ -2,7 +2,12 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" + } + + google-beta = { + source = "hashicorp/google-beta" + version = "5.23.0" } random = { @@ -23,6 +28,10 @@ resource "random_id" "uid" { } resource "google_compute_instance_template" "template" { + # Beta provider is necessary to set confidential instance types. + # TODO(msanft): Remove beta provider once confidential instance type setting is in GA. + provider = google-beta + name = local.name machine_type = var.instance_type tags = ["constellation-${var.uid}"] // Note that this is also applied as a label @@ -33,8 +42,13 @@ resource "google_compute_instance_template" "template" { confidential_instance_config { enable_confidential_compute = true + confidential_instance_type = var.cc_technology } + # If SEV-SNP is used, we have to explicitly select a Milan processor, as per + # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance_template#confidential_instance_type + min_cpu_platform = var.cc_technology == "SEV_SNP" ? "AMD Milan" : null + disk { disk_size_gb = 10 source_image = var.image_id diff --git a/terraform/infrastructure/gcp/modules/instance_group/variables.tf b/terraform/infrastructure/gcp/modules/instance_group/variables.tf index f4b9a7cdb4..5370ec7d15 100644 --- a/terraform/infrastructure/gcp/modules/instance_group/variables.tf +++ b/terraform/infrastructure/gcp/modules/instance_group/variables.tf @@ -99,3 +99,12 @@ variable "custom_endpoint" { type = string description = "Custom endpoint to use for the Kubernetes API server. If not set, the default endpoint will be used." } + +variable "cc_technology" { + type = string + description = "The confidential computing technology to use for the nodes. One of `SEV`, `SEV_SNP`." + validation { + condition = contains(["SEV", "SEV_SNP"], var.cc_technology) + error_message = "The confidential computing technology has to be 'SEV' or 'SEV_SNP'." + } +} diff --git a/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf b/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf index 2589ba1beb..263ee12a37 100644 --- a/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf +++ b/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/infrastructure/gcp/modules/jump_host/main.tf b/terraform/infrastructure/gcp/modules/jump_host/main.tf index a0a2e4c4f7..c1929792b7 100644 --- a/terraform/infrastructure/gcp/modules/jump_host/main.tf +++ b/terraform/infrastructure/gcp/modules/jump_host/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/infrastructure/gcp/modules/loadbalancer/main.tf b/terraform/infrastructure/gcp/modules/loadbalancer/main.tf index 0a5074f535..5c7bab447d 100644 --- a/terraform/infrastructure/gcp/modules/loadbalancer/main.tf +++ b/terraform/infrastructure/gcp/modules/loadbalancer/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/infrastructure/gcp/variables.tf b/terraform/infrastructure/gcp/variables.tf index add9eeffa8..5d158c9ad4 100644 --- a/terraform/infrastructure/gcp/variables.tf +++ b/terraform/infrastructure/gcp/variables.tf @@ -60,3 +60,12 @@ variable "zone" { type = string description = "GCP zone to deploy the cluster in." } + +variable "cc_technology" { + type = string + description = "The confidential computing technology to use for the nodes. One of `SEV`, `SEV_SNP`." + validation { + condition = contains(["SEV", "SEV_SNP"], var.cc_technology) + error_message = "The confidential computing technology has to be 'SEV' or 'SEV_SNP'." + } +} From 3a349b1a15453d3464456a027f77ab1fd19761e1 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:31:16 +0200 Subject: [PATCH 02/30] variant: add SEV-SNP attestation variant --- internal/attestation/variant/variant.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/internal/attestation/variant/variant.go b/internal/attestation/variant/variant.go index 43397a94b5..e71a51480d 100644 --- a/internal/attestation/variant/variant.go +++ b/internal/attestation/variant/variant.go @@ -44,6 +44,7 @@ const ( awsNitroTPM = "aws-nitro-tpm" awsSEVSNP = "aws-sev-snp" gcpSEVES = "gcp-sev-es" + gcpSEVSNP = "gcp-sev-snp" azureTDX = "azure-tdx" azureSEVSNP = "azure-sev-snp" azureTrustedLaunch = "azure-trustedlaunch" @@ -54,7 +55,7 @@ const ( var providerAttestationMapping = map[cloudprovider.Provider][]Variant{ cloudprovider.AWS: {AWSSEVSNP{}, AWSNitroTPM{}}, cloudprovider.Azure: {AzureSEVSNP{}, AzureTDX{}, AzureTrustedLaunch{}}, - cloudprovider.GCP: {GCPSEVES{}}, + cloudprovider.GCP: {GCPSEVES{}, GCPSEVSNP{}}, cloudprovider.QEMU: {QEMUVTPM{}}, cloudprovider.OpenStack: {QEMUVTPM{}}, } @@ -110,6 +111,8 @@ func FromString(oid string) (Variant, error) { return AWSNitroTPM{}, nil case gcpSEVES: return GCPSEVES{}, nil + case gcpSEVSNP: + return GCPSEVSNP{}, nil case azureSEVSNP: return AzureSEVSNP{}, nil case azureTrustedLaunch: @@ -209,6 +212,24 @@ func (GCPSEVES) Equal(other Getter) bool { return other.OID().Equal(GCPSEVES{}.OID()) } +// GCPSEVSNP holds the GCP SEV-SNP OID. +type GCPSEVSNP struct{} + +// OID returns the struct's object identifier. +func (GCPSEVSNP) OID() asn1.ObjectIdentifier { + return asn1.ObjectIdentifier{1, 3, 9900, 3, 2} +} + +// String returns the string representation of the OID. +func (GCPSEVSNP) String() string { + return gcpSEVSNP +} + +// Equal returns true if the other variant is also GCPSEVSNP. +func (GCPSEVSNP) Equal(other Getter) bool { + return other.OID().Equal(GCPSEVSNP{}.OID()) +} + // AzureTDX holds the OID for Azure TDX CVMs. type AzureTDX struct{} From b9a982d1d3ec2b9f9e67233c24d49f728cf5eeda 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 03/30] 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 c653c489c3..8ea071ae89 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 dc4d8fb83c..f635ebbbdc 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 e0e3492dce..a690ba40b9 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 10ac013d14..c7a7ec63c8 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 2168b7f988..56f358d03e 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 013c50edcf..fa9c0c2d0e 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 0000000000..299af595f2 --- /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 5e0ef59ee0..fab69ff29e 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 From 3999cbc1e4b6d722c4a4a61379bf5804b43d74f9 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:32:40 +0200 Subject: [PATCH 04/30] measurements: add GCP SEV-SNP measurements --- .../measurements/measurement-generator/generate.go | 6 ++++-- internal/attestation/measurements/measurements.go | 3 +++ .../attestation/measurements/measurements_enterprise.go | 1 + internal/attestation/measurements/measurements_oss.go | 9 +++++++++ measurement-reader/cmd/main.go | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/internal/attestation/measurements/measurement-generator/generate.go b/internal/attestation/measurements/measurement-generator/generate.go index b552c6b7d2..bdb8e943f6 100644 --- a/internal/attestation/measurements/measurement-generator/generate.go +++ b/internal/attestation/measurements/measurement-generator/generate.go @@ -84,9 +84,9 @@ func main() { log.Println("Found", variant) returnStmtCtr++ // retrieve and validate measurements for the given CSP and image - measuremnts := mustGetMeasurements(ctx, rekor, provider, variant, defaultImage) + measurements := mustGetMeasurements(ctx, rekor, provider, variant, defaultImage) // replace the return statement with a composite literal containing the validated measurements - clause.Values[0] = measurementsCompositeLiteral(measuremnts) + clause.Values[0] = measurementsCompositeLiteral(measurements) } return true }, nil, @@ -267,6 +267,8 @@ func attestationVariantFromGoIdentifier(identifier string) (variant.Variant, err return variant.AWSNitroTPM{}, nil case "GCPSEVES": return variant.GCPSEVES{}, nil + case "GCPSEVSNP": + return variant.GCPSEVSNP{}, nil case "AzureSEVSNP": return variant.AzureSEVSNP{}, nil case "AzureTDX": diff --git a/internal/attestation/measurements/measurements.go b/internal/attestation/measurements/measurements.go index a702706bd3..1bea6174d0 100644 --- a/internal/attestation/measurements/measurements.go +++ b/internal/attestation/measurements/measurements.go @@ -516,6 +516,9 @@ func DefaultsFor(provider cloudprovider.Provider, attestationVariant variant.Var case provider == cloudprovider.GCP && attestationVariant == variant.GCPSEVES{}: return gcp_GCPSEVES.Copy() + case provider == cloudprovider.GCP && attestationVariant == variant.GCPSEVSNP{}: + return gcp_GCPSEVSNP.Copy() + case provider == cloudprovider.OpenStack && attestationVariant == variant.QEMUVTPM{}: return openstack_QEMUVTPM.Copy() diff --git a/internal/attestation/measurements/measurements_enterprise.go b/internal/attestation/measurements/measurements_enterprise.go index a3d09b0493..b8e6990127 100644 --- a/internal/attestation/measurements/measurements_enterprise.go +++ b/internal/attestation/measurements/measurements_enterprise.go @@ -22,6 +22,7 @@ var ( azure_AzureTDX = M{1: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0xb9, 0x41, 0x89, 0x67, 0xb5, 0x5d, 0x99, 0x24, 0xc8, 0x2c, 0xc3, 0x6d, 0xe8, 0x09, 0xac, 0xa7, 0xeb, 0x7b, 0x01, 0xf1, 0x94, 0x03, 0x84, 0xde, 0x25, 0x89, 0xe1, 0x37, 0xb4, 0x51, 0xb4, 0x8e}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x1e, 0xc8, 0x89, 0x5b, 0x93, 0x81, 0xe7, 0x06, 0xc6, 0x7d, 0x8d, 0x30, 0xf1, 0x95, 0x53, 0x64, 0xd7, 0x41, 0x9a, 0x9e, 0x85, 0x04, 0x9f, 0x7e, 0x19, 0xf1, 0x7e, 0x05, 0x1c, 0xc5, 0xe0, 0x4c}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x30, 0x3e, 0x47, 0xd3, 0x52, 0x90, 0x0d, 0x55, 0xdb, 0xad, 0xe3, 0x2a, 0x41, 0x1e, 0xeb, 0xd9, 0x28, 0x59, 0x87, 0xf2, 0x56, 0xcf, 0xdd, 0x60, 0x8a, 0xe2, 0x1a, 0xce, 0xbf, 0x2e, 0xda, 0x76}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} azure_AzureTrustedLaunch M gcp_GCPSEVES = M{1: {Expected: []byte{0x36, 0x95, 0xdc, 0xc5, 0x5e, 0x3a, 0xa3, 0x40, 0x27, 0xc2, 0x77, 0x93, 0xc8, 0x5c, 0x72, 0x3c, 0x69, 0x7d, 0x70, 0x8c, 0x42, 0xd1, 0xf7, 0x3b, 0xd6, 0xfa, 0x4f, 0x26, 0x60, 0x8a, 0x5b, 0x24}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0xce, 0x7d, 0x34, 0x06, 0xe1, 0xde, 0xb3, 0x35, 0x21, 0x98, 0x95, 0xee, 0x33, 0x16, 0xd2, 0x63, 0xf3, 0x20, 0x1f, 0x32, 0xc9, 0x70, 0xde, 0x8c, 0x24, 0x87, 0x65, 0x92, 0xf4, 0x72, 0x11, 0x5d}, ValidationOpt: Enforce}, 6: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x82, 0xb1, 0x4e, 0x09, 0xf0, 0xaf, 0x8a, 0x38, 0xc5, 0x4e, 0x44, 0x4f, 0xe7, 0x5e, 0x1d, 0xbe, 0xca, 0xd2, 0x88, 0xd0, 0x15, 0xd9, 0xef, 0x37, 0x11, 0x75, 0x0a, 0x78, 0x25, 0xad, 0x32, 0x4a}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x38, 0x51, 0xe5, 0xc2, 0x29, 0x86, 0x01, 0xa5, 0x0f, 0xea, 0xd3, 0xeb, 0x46, 0x86, 0xc7, 0x75, 0xae, 0x26, 0xe6, 0x02, 0x7c, 0x4f, 0xdc, 0xc2, 0xfe, 0xd2, 0x9e, 0x8c, 0xc4, 0x55, 0x45, 0x62}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} + gcp_GCPSEVSNP M openstack_QEMUVTPM = M{4: {Expected: []byte{0xba, 0x9a, 0x57, 0xc4, 0xa6, 0xee, 0xc4, 0x0c, 0xe4, 0x78, 0x09, 0x39, 0x7a, 0xb2, 0xa2, 0x71, 0x71, 0x62, 0xcb, 0xd7, 0x75, 0xd9, 0x32, 0x3c, 0xc6, 0x11, 0x77, 0xab, 0xc1, 0x95, 0x34, 0x9b}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x3f, 0x40, 0x48, 0xb6, 0xea, 0x59, 0xe6, 0x80, 0xf6, 0xc8, 0xb0, 0xbe, 0x9b, 0xd3, 0x45, 0x8a, 0x2d, 0x96, 0x99, 0x8d, 0x6b, 0x6a, 0xff, 0xcc, 0x0c, 0xa7, 0x27, 0x1b, 0x04, 0xb8, 0x6f, 0x58}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x9c, 0x9f, 0x5e, 0xf4, 0x18, 0xa8, 0xe9, 0x40, 0x08, 0xf7, 0xd7, 0x89, 0x65, 0x3c, 0x04, 0xd0, 0x1f, 0xc0, 0xaa, 0x07, 0xf5, 0xb3, 0x7a, 0xa3, 0x27, 0x36, 0x1a, 0x0c, 0x65, 0x17, 0x29, 0xdb}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} qemu_QEMUTDX M qemu_QEMUVTPM = M{4: {Expected: []byte{0xb5, 0x42, 0x65, 0x31, 0x43, 0x95, 0x1d, 0x45, 0x1a, 0x8d, 0x75, 0x99, 0xef, 0x71, 0x1f, 0xdd, 0xe3, 0xb6, 0x9c, 0x14, 0x3a, 0x2b, 0x43, 0x04, 0x12, 0x1d, 0x32, 0x85, 0xde, 0xeb, 0xff, 0xd8}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0xc3, 0xdc, 0x39, 0x88, 0x41, 0x3b, 0x41, 0x95, 0xed, 0x68, 0x5d, 0x99, 0x56, 0x0a, 0x0c, 0xa8, 0x20, 0x43, 0x0e, 0x66, 0xc2, 0x34, 0xa7, 0x55, 0x6f, 0x49, 0xb3, 0x68, 0xf5, 0x76, 0x39, 0xca}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x64, 0x54, 0x4f, 0xe0, 0x2f, 0x51, 0x78, 0x7f, 0x06, 0x74, 0x26, 0xd5, 0xdc, 0xb7, 0x91, 0x72, 0x94, 0x0b, 0x52, 0x13, 0x17, 0x8c, 0x08, 0x38, 0xf6, 0x17, 0x83, 0x54, 0x22, 0x9a, 0x49, 0x9d}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} diff --git a/internal/attestation/measurements/measurements_oss.go b/internal/attestation/measurements/measurements_oss.go index 552d6bd261..0ef7ce640c 100644 --- a/internal/attestation/measurements/measurements_oss.go +++ b/internal/attestation/measurements/measurements_oss.go @@ -64,6 +64,15 @@ var ( 13: WithAllBytes(0x00, Enforce, PCRMeasurementLength), uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength), } + gcp_GCPSEVSNP = M{ + 4: PlaceHolderMeasurement(PCRMeasurementLength), + 8: WithAllBytes(0x00, Enforce, PCRMeasurementLength), + 9: PlaceHolderMeasurement(PCRMeasurementLength), + 11: WithAllBytes(0x00, Enforce, PCRMeasurementLength), + 12: PlaceHolderMeasurement(PCRMeasurementLength), + 13: WithAllBytes(0x00, Enforce, PCRMeasurementLength), + uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength), + } openstack_QEMUVTPM = M{ 4: PlaceHolderMeasurement(PCRMeasurementLength), 8: WithAllBytes(0x00, Enforce, PCRMeasurementLength), diff --git a/measurement-reader/cmd/main.go b/measurement-reader/cmd/main.go index 15ce68d2ea..9bdc44332c 100644 --- a/measurement-reader/cmd/main.go +++ b/measurement-reader/cmd/main.go @@ -30,7 +30,7 @@ func main() { var m []sorted.Measurement switch attestationVariant { - case variant.AWSNitroTPM{}, variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.AzureTrustedLaunch{}, variant.GCPSEVES{}, variant.QEMUVTPM{}: + case variant.AWSNitroTPM{}, variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.AzureTrustedLaunch{}, variant.GCPSEVES{}, variant.GCPSEVSNP{}, variant.QEMUVTPM{}: m, err = tpm.Measurements() if err != nil { log.With(slog.Any("error", err)).Error("Failed to read TPM measurements") From 5488ba1357105e1bb83b819f50539bad63fb4565 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:33:15 +0200 Subject: [PATCH 05/30] gcp: separate package for SEV-ES --- internal/attestation/gcp/es/BUILD.bazel | 43 +++++++ internal/attestation/gcp/es/es.go | 45 +++++++ internal/attestation/gcp/es/issuer.go | 33 +++++ .../attestation/gcp/{ => es}/issuer_test.go | 11 +- internal/attestation/gcp/es/validator.go | 59 +++++++++ .../gcp/{ => es}/validator_test.go | 18 +-- internal/attestation/gcp/issuer.go | 87 ------------- internal/attestation/gcp/validator.go | 120 ------------------ 8 files changed, 196 insertions(+), 220 deletions(-) create mode 100644 internal/attestation/gcp/es/BUILD.bazel create mode 100644 internal/attestation/gcp/es/es.go create mode 100644 internal/attestation/gcp/es/issuer.go rename internal/attestation/gcp/{ => es}/issuer_test.go (86%) create mode 100644 internal/attestation/gcp/es/validator.go rename internal/attestation/gcp/{ => es}/validator_test.go (92%) delete mode 100644 internal/attestation/gcp/issuer.go delete mode 100644 internal/attestation/gcp/validator.go diff --git a/internal/attestation/gcp/es/BUILD.bazel b/internal/attestation/gcp/es/BUILD.bazel new file mode 100644 index 0000000000..a7d089412b --- /dev/null +++ b/internal/attestation/gcp/es/BUILD.bazel @@ -0,0 +1,43 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "es", + srcs = [ + "es.go", + "issuer.go", + "validator.go", + ], + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/es", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/attestation", + "//internal/attestation/gcp", + "//internal/attestation/variant", + "//internal/attestation/vtpm", + "//internal/config", + "@com_github_google_go_tpm_tools//client", + "@com_github_google_go_tpm_tools//proto/attest", + ], +) + +go_test( + name = "es_test", + srcs = [ + "issuer_test.go", + "validator_test.go", + ], + embed = [":es"], + deps = [ + "//internal/attestation/gcp", + "//internal/attestation/variant", + "//internal/attestation/vtpm", + "@com_github_google_go_tpm_tools//proto/attest", + "@com_github_googleapis_gax_go_v2//:gax-go", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + "@com_google_cloud_go_compute//apiv1/computepb", + "@org_golang_google_api//option", + "@org_golang_google_protobuf//proto", + ], +) diff --git a/internal/attestation/gcp/es/es.go b/internal/attestation/gcp/es/es.go new file mode 100644 index 0000000000..b5951b9e51 --- /dev/null +++ b/internal/attestation/gcp/es/es.go @@ -0,0 +1,45 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +# GCP SEV-ES Attestation + +Google offers [confidential VMs], utilizing AMD SEV-ES to provide memory encryption. + +AMD SEV-ES doesn't offer much in terms of remote attestation, and following that the VMs don't offer much either, see [their docs] on how to validate a confidential VM for some insights. +However, each VM comes with a [virtual Trusted Platform Module (vTPM)]. +This module can be used to generate VM unique encryption keys or to attest the platform's chain of boot. We can use the vTPM to verify the VM is running on AMD SEV-ES enabled hardware, allowing us to bootstrap a constellation cluster. + +# Issuer + +Generates a TPM attestation key using a Google provided attestation key. +Additionally project ID, zone, and instance name are fetched from the metadata server and attached to the attestation document. + +# Validator + +Verifies the TPM attestation by using a public key provided by Google's API corresponding to the project ID, zone, instance name tuple attached to the attestation document. + +# Problems + + - SEV-ES is somewhat limited when compared to the newer version SEV-SNP + + Comparison of SEV, SEV-ES, and SEV-SNP can be seen on page seven of [AMD's SNP whitepaper] + + - We have to trust Google + + Since the vTPM is provided by Google, and they could do whatever they want with it, we have no save proof of the VMs actually being confidential. + + - The provided vTPM has no endorsement certificate for its attestation key + + Without a certificate signing the authenticity of any endorsement keys we have no way of establishing a chain of trust. + Instead, we have to rely on Google's API to provide us with the public key of the vTPM's endorsement key. + +[confidential VMs]: https://cloud.google.com/compute/confidential-vm/docs/about-cvm +[their docs]: https://cloud.google.com/compute/confidential-vm/docs/monitoring +[virtual Trusted Platform Module (vTPM)]: https://cloud.google.com/security/shielded-cloud/shielded-vm#vtpm +[AMD's SNP whitepaper]: https://www.amd.com/system/files/TechDocs/SEV-SNP-strengthening-vm-isolation-with-integrity-protection-and-more.pdf#page=7 +*/ +package es diff --git a/internal/attestation/gcp/es/issuer.go b/internal/attestation/gcp/es/issuer.go new file mode 100644 index 0000000000..bbee2f5c39 --- /dev/null +++ b/internal/attestation/gcp/es/issuer.go @@ -0,0 +1,33 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package es + +import ( + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + tpmclient "github.com/google/go-tpm-tools/client" +) + +// Issuer for GCP confidential VM attestation. +type Issuer struct { + variant.GCPSEVES + *vtpm.Issuer +} + +// NewIssuer initializes a new GCP Issuer. +func NewIssuer(log attestation.Logger) *Issuer { + return &Issuer{ + Issuer: vtpm.NewIssuer( + vtpm.OpenVTPM, + tpmclient.GceAttestationKeyRSA, + gcp.GCEInstanceInfo(gcp.MetadataClient{}), + log, + ), + } +} diff --git a/internal/attestation/gcp/issuer_test.go b/internal/attestation/gcp/es/issuer_test.go similarity index 86% rename from internal/attestation/gcp/issuer_test.go rename to internal/attestation/gcp/es/issuer_test.go index 4ad64c7a20..4836628553 100644 --- a/internal/attestation/gcp/issuer_test.go +++ b/internal/attestation/gcp/es/issuer_test.go @@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ -package gcp +package es import ( "context" @@ -13,6 +13,7 @@ import ( "io" "testing" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" "github.com/google/go-tpm-tools/proto/attest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -66,7 +67,7 @@ func TestGetGCEInstanceInfo(t *testing.T) { require := require.New(t) var tpm io.ReadWriteCloser - out, err := getGCEInstanceInfo(tc.client)(context.Background(), tpm, nil) + out, err := gcp.GCEInstanceInfo(tc.client)(context.Background(), tpm, nil) if tc.wantErr { assert.Error(err) } else { @@ -90,14 +91,14 @@ type fakeMetadataClient struct { zoneErr error } -func (c fakeMetadataClient) projectID() (string, error) { +func (c fakeMetadataClient) ProjectID() (string, error) { return c.projectIDString, c.projecIDErr } -func (c fakeMetadataClient) instanceName() (string, error) { +func (c fakeMetadataClient) InstanceName() (string, error) { return c.instanceNameString, c.instanceNameErr } -func (c fakeMetadataClient) zone() (string, error) { +func (c fakeMetadataClient) Zone() (string, error) { return c.zoneString, c.zoneErr } diff --git a/internal/attestation/gcp/es/validator.go b/internal/attestation/gcp/es/validator.go new file mode 100644 index 0000000000..4177b6f0af --- /dev/null +++ b/internal/attestation/gcp/es/validator.go @@ -0,0 +1,59 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package es + +import ( + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/google/go-tpm-tools/proto/attest" +) + +const minimumGceVersion = 1 + +// Validator for GCP confidential VM attestation. +type Validator struct { + variant.GCPSEVES + *vtpm.Validator +} + +// NewValidator initializes a new GCP validator with the provided PCR values specified in the config. +func NewValidator(cfg *config.GCPSEVES, log attestation.Logger) (*Validator, error) { + getTrustedKey, err := gcp.TrustedKeyGetter(variant.GCPSEVES{}, gcp.NewRESTClient) + if err != nil { + return nil, fmt.Errorf("create trusted key getter: %v", err) + } + + return &Validator{ + Validator: vtpm.NewValidator( + cfg.Measurements, + getTrustedKey, + validateCVM, + log, + ), + }, nil +} + +// validateCVM checks that the machine state represents a GCE AMD-SEV VM. +func validateCVM(_ vtpm.AttestationDocument, state *attest.MachineState) error { + gceVersion := state.Platform.GetGceVersion() + if gceVersion < minimumGceVersion { + return fmt.Errorf("outdated GCE version: %v (require >= %v)", gceVersion, minimumGceVersion) + } + + tech := state.Platform.Technology + wantTech := attest.GCEConfidentialTechnology_AMD_SEV + if tech != wantTech { + return fmt.Errorf("unexpected confidential technology: %v (expected: %v)", tech, wantTech) + } + + return nil +} diff --git a/internal/attestation/gcp/validator_test.go b/internal/attestation/gcp/es/validator_test.go similarity index 92% rename from internal/attestation/gcp/validator_test.go rename to internal/attestation/gcp/es/validator_test.go index 203809a4ff..3eba824027 100644 --- a/internal/attestation/gcp/validator_test.go +++ b/internal/attestation/gcp/es/validator_test.go @@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ -package gcp +package es import ( "context" @@ -14,6 +14,8 @@ import ( "testing" "cloud.google.com/go/compute/apiv1/computepb" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/google/go-tpm-tools/proto/attest" "github.com/googleapis/gax-go/v2" @@ -87,7 +89,7 @@ Y+t5OxL3kL15VzY1Ob0d5cMCAwEAAQ== testCases := map[string]struct { instanceInfo []byte - getClient func(ctx context.Context, opts ...option.ClientOption) (gcpRestClient, error) + getClient func(ctx context.Context, opts ...option.ClientOption) (gcp.GCPRESTClient, error) wantErr bool }{ "success": { @@ -146,12 +148,12 @@ Y+t5OxL3kL15VzY1Ob0d5cMCAwEAAQ== t.Run(name, func(t *testing.T) { assert := assert.New(t) - v := &Validator{ - restClient: tc.getClient, - } attDoc := vtpm.AttestationDocument{InstanceInfo: tc.instanceInfo} - out, err := v.trustedKeyFromGCEAPI(context.Background(), attDoc, nil) + getTrustedKey, err := gcp.TrustedKeyGetter(variant.GCPSEVES{}, tc.getClient) + require.NoError(t, err) + + out, err := getTrustedKey(context.Background(), attDoc, nil) if tc.wantErr { assert.Error(err) @@ -175,8 +177,8 @@ type fakeInstanceClient struct { ident *computepb.ShieldedInstanceIdentity } -func prepareFakeClient(ident *computepb.ShieldedInstanceIdentity, newErr, getIdentErr error) func(ctx context.Context, opts ...option.ClientOption) (gcpRestClient, error) { - return func(_ context.Context, _ ...option.ClientOption) (gcpRestClient, error) { +func prepareFakeClient(ident *computepb.ShieldedInstanceIdentity, newErr, getIdentErr error) func(ctx context.Context, opts ...option.ClientOption) (gcp.GCPRESTClient, error) { + return func(_ context.Context, _ ...option.ClientOption) (gcp.GCPRESTClient, error) { return &fakeInstanceClient{ getIdentErr: getIdentErr, ident: ident, diff --git a/internal/attestation/gcp/issuer.go b/internal/attestation/gcp/issuer.go deleted file mode 100644 index 4dc36ba0dd..0000000000 --- a/internal/attestation/gcp/issuer.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package gcp - -import ( - "context" - "encoding/json" - "errors" - "io" - - "cloud.google.com/go/compute/metadata" - "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" - tpmclient "github.com/google/go-tpm-tools/client" - "github.com/google/go-tpm-tools/proto/attest" -) - -// Issuer for GCP confidential VM attestation. -type Issuer struct { - variant.GCPSEVES - *vtpm.Issuer -} - -// NewIssuer initializes a new GCP Issuer. -func NewIssuer(log attestation.Logger) *Issuer { - return &Issuer{ - Issuer: vtpm.NewIssuer( - vtpm.OpenVTPM, - tpmclient.GceAttestationKeyRSA, - getGCEInstanceInfo(metadataClient{}), - log, - ), - } -} - -// getGCEInstanceInfo fetches VM metadata used for attestation. -func getGCEInstanceInfo(client gcpMetadataClient) func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) { - // Ideally we would want to use the endorsement public key certificate - // However, this is not available on GCE instances - // Workaround: Provide ShieldedVM instance info - // The attestating party can request the VMs signing key using Google's API - return func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) { - projectID, err := client.projectID() - if err != nil { - return nil, errors.New("unable to fetch projectID") - } - zone, err := client.zone() - if err != nil { - return nil, errors.New("unable to fetch zone") - } - instanceName, err := client.instanceName() - if err != nil { - return nil, errors.New("unable to fetch instance name") - } - - return json.Marshal(&attest.GCEInstanceInfo{ - Zone: zone, - ProjectId: projectID, - InstanceName: instanceName, - }) - } -} - -type gcpMetadataClient interface { - projectID() (string, error) - instanceName() (string, error) - zone() (string, error) -} - -type metadataClient struct{} - -func (c metadataClient) projectID() (string, error) { - return metadata.ProjectID() -} - -func (c metadataClient) instanceName() (string, error) { - return metadata.InstanceName() -} - -func (c metadataClient) zone() (string, error) { - return metadata.Zone() -} diff --git a/internal/attestation/gcp/validator.go b/internal/attestation/gcp/validator.go deleted file mode 100644 index 310a33b556..0000000000 --- a/internal/attestation/gcp/validator.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package gcp - -import ( - "context" - "crypto" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - - compute "cloud.google.com/go/compute/apiv1" - "cloud.google.com/go/compute/apiv1/computepb" - "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" - "github.com/edgelesssys/constellation/v2/internal/config" - "github.com/google/go-tpm-tools/proto/attest" - "github.com/googleapis/gax-go/v2" - "google.golang.org/api/option" -) - -const minimumGceVersion = 1 - -// Validator for GCP confidential VM attestation. -type Validator struct { - variant.GCPSEVES - *vtpm.Validator - - restClient func(context.Context, ...option.ClientOption) (gcpRestClient, error) -} - -// NewValidator initializes a new GCP validator with the provided PCR values. -func NewValidator(cfg *config.GCPSEVES, log attestation.Logger) *Validator { - v := &Validator{ - restClient: newInstanceClient, - } - v.Validator = vtpm.NewValidator( - cfg.Measurements, - v.trustedKeyFromGCEAPI, - validateCVM, - log, - ) - - return v -} - -type gcpRestClient interface { - GetShieldedInstanceIdentity(ctx context.Context, req *computepb.GetShieldedInstanceIdentityInstanceRequest, opts ...gax.CallOption) (*computepb.ShieldedInstanceIdentity, error) - Close() error -} - -type instanceClient struct { - *compute.InstancesClient -} - -func newInstanceClient(ctx context.Context, opts ...option.ClientOption) (gcpRestClient, error) { - c, err := compute.NewInstancesRESTClient(ctx, opts...) - if err != nil { - return nil, err - } - return &instanceClient{c}, nil -} - -// trustedKeyFromGCEAPI queries the GCE API for a shieldedVM's public signing key. -// This key can be used to verify attestation statements issued by the VM. -func (v *Validator) trustedKeyFromGCEAPI(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { - client, err := v.restClient(ctx) - if err != nil { - return nil, fmt.Errorf("creating GCE client: %w", err) - } - defer client.Close() - - var instanceInfo attest.GCEInstanceInfo - if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil { - return nil, err - } - - instance, err := client.GetShieldedInstanceIdentity(ctx, &computepb.GetShieldedInstanceIdentityInstanceRequest{ - Instance: instanceInfo.GetInstanceName(), - Project: instanceInfo.GetProjectId(), - Zone: instanceInfo.GetZone(), - }) - if err != nil { - return nil, fmt.Errorf("retrieving VM identity: %w", err) - } - - if instance.SigningKey == nil || instance.SigningKey.EkPub == nil { - return nil, fmt.Errorf("received no signing key from GCP API") - } - - // Parse the signing key return by GetShieldedInstanceIdentity - block, _ := pem.Decode([]byte(*instance.SigningKey.EkPub)) - if block == nil || block.Type != "PUBLIC KEY" { - return nil, fmt.Errorf("failed to decode PEM block containing public key") - } - - return x509.ParsePKIXPublicKey(block.Bytes) -} - -// validateCVM checks that the machine state represents a GCE AMD-SEV VM. -func validateCVM(_ vtpm.AttestationDocument, state *attest.MachineState) error { - gceVersion := state.Platform.GetGceVersion() - if gceVersion < minimumGceVersion { - return fmt.Errorf("outdated GCE version: %v (require >= %v)", gceVersion, minimumGceVersion) - } - - tech := state.Platform.Technology - wantTech := attest.GCEConfidentialTechnology_AMD_SEV - if tech != wantTech { - return fmt.Errorf("unexpected confidential technology: %v (expected: %v)", tech, wantTech) - } - - return nil -} From afeac3a8e9c83fa454bf3fe379cf3ad1d494c45d Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:33:55 +0200 Subject: [PATCH 06/30] attestation: add GCP SEV-SNP attestation logic --- internal/attestation/gcp/snp/BUILD.bazel | 56 ++++ internal/attestation/gcp/snp/issuer.go | 160 ++++++++++ internal/attestation/gcp/snp/issuer_test.go | 44 +++ internal/attestation/gcp/snp/snp.go | 44 +++ internal/attestation/gcp/snp/validator.go | 231 ++++++++++++++ .../attestation/gcp/snp/validator_test.go | 295 ++++++++++++++++++ internal/attestation/snp/BUILD.bazel | 1 + internal/attestation/snp/snp.go | 2 + internal/attestation/vtpm/attestation.go | 23 +- 9 files changed, 851 insertions(+), 5 deletions(-) create mode 100644 internal/attestation/gcp/snp/BUILD.bazel create mode 100644 internal/attestation/gcp/snp/issuer.go create mode 100644 internal/attestation/gcp/snp/issuer_test.go create mode 100644 internal/attestation/gcp/snp/snp.go create mode 100644 internal/attestation/gcp/snp/validator.go create mode 100644 internal/attestation/gcp/snp/validator_test.go diff --git a/internal/attestation/gcp/snp/BUILD.bazel b/internal/attestation/gcp/snp/BUILD.bazel new file mode 100644 index 0000000000..3056543f4d --- /dev/null +++ b/internal/attestation/gcp/snp/BUILD.bazel @@ -0,0 +1,56 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "snp", + srcs = [ + "issuer.go", + "snp.go", + "validator.go", + ], + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/snp", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/attestation", + "//internal/attestation/gcp", + "//internal/attestation/snp", + "//internal/attestation/variant", + "//internal/attestation/vtpm", + "//internal/config", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//client", + "@com_github_google_go_sev_guest//kds", + "@com_github_google_go_sev_guest//proto/sevsnp", + "@com_github_google_go_sev_guest//validate", + "@com_github_google_go_sev_guest//verify", + "@com_github_google_go_sev_guest//verify/trust", + "@com_github_google_go_tpm//legacy/tpm2", + "@com_github_google_go_tpm_tools//client", + "@com_github_google_go_tpm_tools//proto/attest", + ], +) + +go_test( + name = "snp_test", + srcs = [ + "issuer_test.go", + "validator_test.go", + ], + embed = [":snp"], + deps = [ + "//internal/attestation", + "//internal/attestation/aws/snp/testdata", + "//internal/attestation/simulator", + "//internal/attestation/snp", + "//internal/attestation/vtpm", + "//internal/config", + "//internal/logger", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//proto/sevsnp", + "@com_github_google_go_sev_guest//verify", + "@com_github_google_go_tpm_tools//client", + "@com_github_google_go_tpm_tools//proto/attest", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go new file mode 100644 index 0000000000..951253e778 --- /dev/null +++ b/internal/attestation/gcp/snp/issuer.go @@ -0,0 +1,160 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "context" + "crypto/sha512" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + + "github.com/google/go-sev-guest/abi" + sevclient "github.com/google/go-sev-guest/client" + "github.com/google/go-tpm-tools/client" + tpmclient "github.com/google/go-tpm-tools/client" + "github.com/google/go-tpm-tools/proto/attest" +) + +// Issuer issues SEV-SNP attestations. +type Issuer struct { + variant.GCPSEVSNP + *vtpm.Issuer +} + +// NewIssuer creates a SEV-SNP based issuer for GCP. +func NewIssuer(log attestation.Logger) *Issuer { + return &Issuer{ + Issuer: vtpm.NewIssuer( + vtpm.OpenVTPM, + getAttestationKey, + getInstanceInfo, + log, + ), + } +} + +// getAttestationKey returns a new attestation key. +func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { + tpmAk, err := client.GceAttestationKeyRSA(tpm) + if err != nil { + return nil, fmt.Errorf("creating RSA Endorsement key: %w", err) + } + + return tpmAk, nil +} + +// getInstanceInfo generates an extended SNP report, i.e. the report and any loaded certificates. +// Report generation is triggered by sending ioctl syscalls to the SNP guest device, the AMD PSP generates the report. +// The returned bytes will be written into the attestation document. +func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) { + tpmAk, err := client.GceAttestationKeyRSA(tpm) + if err != nil { + return nil, fmt.Errorf("creating RSA Endorsement key: %w", err) + } + + encoded, err := x509.MarshalPKIXPublicKey(tpmAk.PublicKey()) + if err != nil { + return nil, fmt.Errorf("marshalling public key: %w", err) + } + + akDigest := sha512.Sum512(encoded) + + device, err := sevclient.OpenDevice() + if err != nil { + return nil, fmt.Errorf("opening sev device: %w", err) + } + defer device.Close() + + report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, akDigest, 0) + if err != nil { + return nil, fmt.Errorf("getting extended report: %w", err) + } + + vcek, err := pemEncodedVCEK(certs) + if err != nil { + return nil, fmt.Errorf("parsing vlek: %w", err) + } + + gceInstanceInfo, err := gceInstanceInfo() + if err != nil { + return nil, fmt.Errorf("getting GCE instance info: %w", err) + } + + raw, err := json.Marshal(snp.InstanceInfo{ + AttestationReport: report, + ReportSigner: vcek, + GCP: gceInstanceInfo, + }) + if err != nil { + return nil, fmt.Errorf("marshalling instance info: %w", err) + } + + return raw, nil +} + +// gceInstanceInfo returns the instance info for a GCE instance from the metadata API. +func gceInstanceInfo() (*attest.GCEInstanceInfo, error) { + c := gcp.MetadataClient{} + + instanceName, err := c.InstanceName() + if err != nil { + return nil, fmt.Errorf("getting instance name: %w", err) + } + + projectID, err := c.ProjectID() + if err != nil { + return nil, fmt.Errorf("getting project ID: %w", err) + } + + zone, err := c.Zone() + if err != nil { + return nil, fmt.Errorf("getting zone: %w", err) + } + + return &attest.GCEInstanceInfo{ + InstanceName: instanceName, + ProjectId: projectID, + Zone: zone, + }, nil +} + +// pemEncodedVCEK takes a marshalled SNP certificate table and returns the PEM-encoded VCEK certificate. +// AMD documentation on certificate tables can be found in section 4.1.8.1, revision 2.03 "SEV-ES Guest-Hypervisor Communication Block Standardization". +// https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/56421.pdf +func pemEncodedVCEK(certs []byte) ([]byte, error) { + certTable := abi.CertTable{} + if err := certTable.Unmarshal(certs); err != nil { + return nil, fmt.Errorf("unmarshalling SNP certificate table: %w", err) + } + + vcekRaw, err := certTable.GetByGUIDString(abi.VcekGUID) + if err != nil { + return nil, fmt.Errorf("getting VCEK certificate: %w", err) + } + + // An optional check for certificate well-formedness. vlekRaw == cert.Raw. + cert, err := x509.ParseCertificate(vcekRaw) + if err != nil { + return nil, fmt.Errorf("parsing certificate: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + + return certPEM, nil +} diff --git a/internal/attestation/gcp/snp/issuer_test.go b/internal/attestation/gcp/snp/issuer_test.go new file mode 100644 index 0000000000..3f2f246990 --- /dev/null +++ b/internal/attestation/gcp/snp/issuer_test.go @@ -0,0 +1,44 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "os" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/simulator" + tpmclient "github.com/google/go-tpm-tools/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAttestationKey(t *testing.T) { + cgo := os.Getenv("CGO_ENABLED") + if cgo == "0" { + t.Skip("skipping test because CGO is disabled and tpm simulator requires it") + } + + require := require.New(t) + assert := assert.New(t) + + tpm, err := simulator.OpenSimulatedTPM() + require.NoError(err) + defer tpm.Close() + + // create the attestation key in RSA format + tpmAk, err := tpmclient.AttestationKeyRSA(tpm) + assert.NoError(err) + assert.NotNil(tpmAk) + + // get the cached, already created key + getAk, err := getAttestationKey(tpm) + assert.NoError(err) + assert.NotNil(getAk) + + // if everything worked fine, tpmAk and getAk are the same key + assert.Equal(tpmAk, getAk) +} diff --git a/internal/attestation/gcp/snp/snp.go b/internal/attestation/gcp/snp/snp.go new file mode 100644 index 0000000000..f81a6df2cf --- /dev/null +++ b/internal/attestation/gcp/snp/snp.go @@ -0,0 +1,44 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +# GCP SEV-SNP Attestation + +Google offers [confidential VMs], utilizing AMD SEV-SNP to provide memory encryption. + +Each SEV-SNP VM comes with a [virtual Trusted Platform Module (vTPM)]. +This vTPM can be used to generate encryption keys unique to the VM or to attest the platform's boot chain. +We can use the vTPM to verify the VM is running on AMD SEV-SNP enabled hardware and booted the expected OS image, allowing us to bootstrap a constellation cluster. + +# Issuer + +Retrieves an SEV-SNP attestation statement for the VM it's running in. Then, it generates a TPM attestation statement, binding the SEV-SNP attestation statement to it by including its hash in the TPM attestation statement. +Without binding the SEV-SNP attestation statement to the TPM attestation statement, the SEV-SNP attestation statement could be used in a different VM. Furthermore, it's important to first create the SEV-SNP attestation statement +and then the TPM attestation statement, as otherwise, a non-CVM could be used to create a valid TPM attestation statement, and then later swap the SEV-SNP attestation statement with one from a CVM. +Additionally project ID, zone, and instance name are fetched from the metadata server and attached to the attestation statement. + +# Validator + +First, it verifies the SEV-SNP attestation statement by checking the signatures and claims. Then, it verifies the TPM attestation by using a +public key provided by Google's API corresponding to the project ID, zone, instance name tuple attached to the attestation document, and confirms whether the SEV-SNP attestation statement is bound to the TPM attestation statement. + +# Problems + + - We have to trust Google + + Since the vTPM is provided by Google, and they could do whatever they want with it, we have no save proof of the VMs actually being confidential. + + - The provided vTPM has no endorsement certificate for its attestation key + + Without a certificate signing the authenticity of any endorsement keys we have no way of establishing a chain of trust. + Instead, we have to rely on Google's API to provide us with the public key of the vTPM's endorsement key. + +[GCP Confidential VMs]: https://cloud.google.com/compute/confidential-vm/docs/about-cvm +[GCP Virtual Trusted Platform Module (vTPM)]: https://cloud.google.com/security/shielded-cloud/shielded-vm#vtpm +[GCP Monitoring docs]: https://cloud.google.com/compute/confidential-vm/docs/monitoring +[AMD SEV-SNP whitepaper]: https://www.amd.com/system/files/TechDocs/SEV-SNP-strengthening-vm-isolation-with-integrity-protection-and-more.pdf#page=7 +*/ +package snp diff --git a/internal/attestation/gcp/snp/validator.go b/internal/attestation/gcp/snp/validator.go new file mode 100644 index 0000000000..a2f4afeb92 --- /dev/null +++ b/internal/attestation/gcp/snp/validator.go @@ -0,0 +1,231 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "context" + "crypto" + "crypto/sha512" + "crypto/x509" + "encoding/json" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/kds" + "github.com/google/go-sev-guest/proto/sevsnp" + "github.com/google/go-sev-guest/validate" + "github.com/google/go-sev-guest/verify" + "github.com/google/go-sev-guest/verify/trust" + "github.com/google/go-tpm-tools/proto/attest" + "github.com/google/go-tpm/legacy/tpm2" +) + +// Validator for GCP SEV-SNP / TPM attestation. +type Validator struct { + variant.GCPSEVSNP + *vtpm.Validator + cfg *config.GCPSEVSNP + + // reportValidator validates a SNP report and is required for testing. + reportValidator snpReportValidator + + // gceKeyGetter gets the public key of the EK from the GCE metadata API. + gceKeyGetter func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) + + log attestation.Logger +} + +// NewValidator creates a new Validator. +func NewValidator(cfg *config.GCPSEVSNP, log attestation.Logger) (*Validator, error) { + getGCEKey, err := gcp.TrustedKeyGetter(variant.GCPSEVSNP{}, gcp.NewRESTClient) + if err != nil { + return nil, fmt.Errorf("create trusted key getter: %v", err) + } + + v := &Validator{ + cfg: cfg, + reportValidator: &gcpValidator{httpsGetter: trust.DefaultHTTPSGetter(), verifier: &reportVerifierImpl{}, validator: &reportValidatorImpl{}}, + gceKeyGetter: getGCEKey, + log: log, + } + + v.Validator = vtpm.NewValidator( + cfg.Measurements, + v.getTrustedKey, + v.validateCVM, + log, + ) + return v, nil +} + +// getTrustedKey returns TPM endorsement key provided through the GCE metadata API. +func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { + ekPub, err := v.gceKeyGetter(ctx, attDoc, nil) + if err != nil { + return nil, fmt.Errorf("getting TPM endorsement key: %w", err) + } + + return ekPub, nil +} + +// validateCVM validates the SEV-SNP attestation document. +func (v *Validator) validateCVM(attDoc vtpm.AttestationDocument, state *attest.MachineState) error { + pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub) + if err != nil { + return fmt.Errorf("decoding public area: %w", err) + } + + pubKey, err := pubArea.Key() + if err != nil { + return fmt.Errorf("getting public key: %w", err) + } + + akDigest, err := sha512sum(pubKey) + if err != nil { + return fmt.Errorf("calculating hash of attestation key: %w", err) + } + + if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), akDigest, v.cfg, v.log); err != nil { + return fmt.Errorf("validating SNP report: %w", err) + } + return nil +} + +// sha512sum PEM-encodes a public key and calculates the SHA512 hash of the encoded key. +func sha512sum(key crypto.PublicKey) ([64]byte, error) { + pub, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return [64]byte{}, fmt.Errorf("marshalling public key: %w", err) + } + + return sha512.Sum512(pub), nil +} + +// snpReportValidator validates a given SNP report. +type snpReportValidator interface { + validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, ak [64]byte, config *config.GCPSEVSNP, log attestation.Logger) error +} + +// gcpValidator implements the validation for GCP SEV-SNP attestation. +// The properties exist for unittesting. +type gcpValidator struct { + verifier reportVerifier + validator reportValidator + httpsGetter trust.HTTPSGetter +} + +type reportVerifier interface { + SnpAttestation(att *sevsnp.Attestation, opts *verify.Options) error +} +type reportValidator interface { + SnpAttestation(att *sevsnp.Attestation, opts *validate.Options) error +} + +type reportValidatorImpl struct{} + +func (r *reportValidatorImpl) SnpAttestation(att *sevsnp.Attestation, opts *validate.Options) error { + return validate.SnpAttestation(att, opts) +} + +type reportVerifierImpl struct{} + +func (r *reportVerifierImpl) SnpAttestation(att *sevsnp.Attestation, opts *verify.Options) error { + return verify.SnpAttestation(att, opts) +} + +// validate the report by checking if it has a valid VCEK signature. +// The certificate chain ARK -> ASK -> VCEK is also validated. +// Checks that the report's userData matches the connection's userData. +func (a *gcpValidator) validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, akDigest [64]byte, config *config.GCPSEVSNP, log attestation.Logger) error { + var info snp.InstanceInfo + if err := json.Unmarshal(attestation.InstanceInfo, &info); err != nil { + return fmt.Errorf("unmarshalling instance info: %w", err) + } + + certchain := snp.NewCertificateChain(ask, ark) + + att, err := info.AttestationWithCerts(a.httpsGetter, certchain, log) + if err != nil { + return fmt.Errorf("getting attestation with certs: %w", err) + } + + verifyOpts, err := getVerifyOpts(att) + if err != nil { + return fmt.Errorf("getting verify options: %w", err) + } + + if err := a.verifier.SnpAttestation(att, verifyOpts); err != nil { + return fmt.Errorf("verifying SNP attestation: %w", err) + } + + validateOpts := &validate.Options{ + // Check that the attestation key's digest is included in the report. + ReportData: akDigest[:], + GuestPolicy: abi.SnpPolicy{ + Debug: false, // Debug means the VM can be decrypted by the host for debugging purposes and thus is not allowed. + SMT: true, // Allow Simultaneous Multi-Threading (SMT). Normally, we would want to disable SMT + // but GCP machines are currently facing issues if it's disabled + }, + VMPL: new(int), // Checks that Virtual Machine Privilege Level (VMPL) is 0. + // This checks that the reported LaunchTCB version is equal or greater than the minimum specified in the config. + // We don't specify Options.MinimumTCB as it only restricts the allowed TCB for Current_ and Reported_TCB. + // Because we allow Options.ProvisionalFirmware, there is not security gained in also checking Current_ and Reported_TCB. + // We always have to check Launch_TCB as this value indicated the smallest TCB version a VM has seen during + // it's lifetime. + MinimumLaunchTCB: kds.TCBParts{ + BlSpl: config.BootloaderVersion.Value, // Bootloader + TeeSpl: config.TEEVersion.Value, // TEE (Secure OS) + SnpSpl: config.SNPVersion.Value, // SNP + UcodeSpl: config.MicrocodeVersion.Value, // Microcode + }, + // Check that CurrentTCB >= CommittedTCB. + PermitProvisionalFirmware: true, + } + + // Checks if the attestation report matches the given constraints. + // Some constraints are implicitly checked by validate.SnpAttestation: + // - the report is not expired + if err := a.validator.SnpAttestation(att, validateOpts); err != nil { + return fmt.Errorf("validating SNP attestation: %w", err) + } + + return nil +} + +func getVerifyOpts(att *sevsnp.Attestation) (*verify.Options, error) { + ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) + if err != nil { + return &verify.Options{}, fmt.Errorf("parsing ASK certificate: %w", err) + } + ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) + if err != nil { + return &verify.Options{}, fmt.Errorf("parsing ARK certificate: %w", err) + } + + verifyOpts := &verify.Options{ + DisableCertFetching: true, + TrustedRoots: map[string][]*trust.AMDRootCerts{ + "Milan": { + { + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + Ask: ask, + Ark: ark, + }, + }, + }, + }, + } + + return verifyOpts, nil +} diff --git a/internal/attestation/gcp/snp/validator_test.go b/internal/attestation/gcp/snp/validator_test.go new file mode 100644 index 0000000000..5772c357ed --- /dev/null +++ b/internal/attestation/gcp/snp/validator_test.go @@ -0,0 +1,295 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/aws/snp/testdata" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/proto/sevsnp" + spb "github.com/google/go-sev-guest/proto/sevsnp" + "github.com/google/go-sev-guest/verify" + "github.com/google/go-tpm-tools/proto/attest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTrustedKey(t *testing.T) { + validator := func() *Validator { return &Validator{reportValidator: stubGCPValidator{}} } + testCases := map[string]struct { + akPub []byte + info []byte + wantErr bool + }{ + "null byte docs": { + akPub: []byte{0x00, 0x00, 0x00, 0x00}, + info: []byte{0x00, 0x00, 0x00, 0x00}, + wantErr: true, + }, + "nil": { + akPub: nil, + info: nil, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + out, err := validator().getTrustedKey( + context.Background(), + vtpm.AttestationDocument{ + Attestation: &attest.Attestation{ + AkPub: tc.akPub, + }, + InstanceInfo: tc.info, + }, + nil, + ) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + + assert.Nil(out) + }) + } +} + +// TestValidateSNPReport has to setup the following to run ValidateSNPReport: +// - parse ARK certificate from constants.go. +// - parse cached ASK certificate. +// - parse cached SNP report. +// - parse cached AK hash. Hash and SNP report have to match. +// - parse cache VLEK cert. +func TestValidateSNPReport(t *testing.T) { + require := require.New(t) + certs, err := loadCerts(testdata.CertChain) + require.NoError(err) + ark := certs[1] + ask := certs[0] + + // reportTransformer unpacks the base64 encoded report, applies the given transformations and re-encodes it. + reportTransformer := func(reportHex string, transformations func(*spb.Report)) string { + rawReport, err := base64.StdEncoding.DecodeString(reportHex) + require.NoError(err) + report, err := abi.ReportToProto(rawReport) + require.NoError(err) + transformations(report) + reportBytes, err := abi.ReportToAbiBytes(report) + require.NoError(err) + return base64.StdEncoding.EncodeToString(reportBytes) + } + + testCases := map[string]struct { + ak string + report string + reportTransformer func(string, func(*spb.Report)) string + verifier reportVerifier + validator reportValidator + wantErr bool + }{ + "success": { + ak: testdata.AKDigest, + report: testdata.SNPReport, + verifier: &reportVerifierImpl{}, + validator: &reportValidatorImpl{}, + }, + "invalid report data": { + ak: testdata.AKDigest, + report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { + r.ReportData = make([]byte, 64) + }), + verifier: &stubReportVerifier{}, + validator: &reportValidatorImpl{}, + wantErr: true, + }, + "invalid report signature": { + ak: testdata.AKDigest, + report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { r.Signature[0]++ }), + verifier: &reportVerifierImpl{}, + validator: &reportValidatorImpl{}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + hash, err := hex.DecodeString(tc.ak) + require.NoError(err) + + report, err := base64.StdEncoding.DecodeString(tc.report) + require.NoError(err) + + info := snp.InstanceInfo{AttestationReport: report, ReportSigner: testdata.VLEK} + infoMarshalled, err := json.Marshal(info) + require.NoError(err) + + v := gcpValidator{httpsGetter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), verifier: tc.verifier, validator: tc.validator} + err = v.validate(vtpm.AttestationDocument{InstanceInfo: infoMarshalled}, ask, ark, [64]byte(hash), config.DefaultForGCPSEVSNP(), logger.NewTest(t)) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} + +type stubHTTPSGetter struct { + urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs + err error +} + +func newStubHTTPSGetter(urlResponseMatcher *urlResponseMatcher, err error) *stubHTTPSGetter { + return &stubHTTPSGetter{ + urlResponseMatcher: urlResponseMatcher, + err: err, + } +} + +func (s *stubHTTPSGetter) Get(url string) ([]byte, error) { + if s.err != nil { + return nil, s.err + } + return s.urlResponseMatcher.match(url) +} + +type urlResponseMatcher struct { + certChainResponse []byte + wantCertChainRequest bool + vcekResponse []byte + wantVcekRequest bool +} + +func (m *urlResponseMatcher) match(url string) ([]byte, error) { + switch { + case url == "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": + if !m.wantCertChainRequest { + return nil, fmt.Errorf("unexpected cert_chain request") + } + return m.certChainResponse, nil + case regexp.MustCompile(`https:\/\/kdsintf.amd.com\/vcek\/v1\/Milan\/.*`).MatchString(url): + if !m.wantVcekRequest { + return nil, fmt.Errorf("unexpected VCEK request") + } + return m.vcekResponse, nil + default: + return nil, fmt.Errorf("unexpected URL: %s", url) + } +} + +func TestSha512sum(t *testing.T) { + testCases := map[string]struct { + key string + hash string + match bool + }{ + "success": { + // Generated using: rsa.GenerateKey(rand.Reader, 1024). + key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", + hash: "2d6fe5ec59d7240b8a4c27c2ff27ba1071105fa50d45543768fcbabf9ee3cb8f8fa0afa51e08e053af30f6d11066ebfd47e75bda5ccc085c115d7e1896f3c62f", + match: true, + }, + "mismatching hash": { + key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", + hash: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + match: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + newKey, err := loadKeyFromHex(tc.key) + require.NoError(err) + + // Function under test: + hash, err := sha512sum(newKey) + assert.NoError(err) + + expected, err := hex.DecodeString(tc.hash) + require.NoError(err) + + if tc.match { + assert.True(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected hash %x, got %x", expected, hash)) + } else { + assert.False(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected mismatching hashes, got %x", hash)) + } + }) + } +} + +func loadKeyFromHex(key string) (crypto.PublicKey, error) { + decoded, err := hex.DecodeString(key) + if err != nil { + return nil, err + } + + return x509.ParsePKIXPublicKey(decoded) +} + +// loadCachedCertChain loads a valid ARK and ASK from the testdata folder. +func loadCerts(pemData []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + + for len(pemData) > 0 { + var block *pem.Block + block, pemData = pem.Decode(pemData) + if block == nil { + break + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + certs = append(certs, cert) + } + + if len(certs) == 0 { + return nil, errors.New("no valid certificates found") + } + + return certs, nil +} + +type stubGCPValidator struct{} + +func (stubGCPValidator) validate(_ vtpm.AttestationDocument, _ *x509.Certificate, _ *x509.Certificate, _ [64]byte, _ *config.GCPSEVSNP, _ attestation.Logger) error { + return nil +} + +type stubReportVerifier struct{} + +func (stubReportVerifier) SnpAttestation(_ *sevsnp.Attestation, _ *verify.Options) error { + return nil +} diff --git a/internal/attestation/snp/BUILD.bazel b/internal/attestation/snp/BUILD.bazel index 700a3aa865..1a1bd25044 100644 --- a/internal/attestation/snp/BUILD.bazel +++ b/internal/attestation/snp/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "@com_github_google_go_sev_guest//kds", "@com_github_google_go_sev_guest//proto/sevsnp", "@com_github_google_go_sev_guest//verify/trust", + "@com_github_google_go_tpm_tools//proto/attest", ], ) diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index 95cba55bfc..e9db5f5a54 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -20,6 +20,7 @@ import ( "github.com/google/go-sev-guest/kds" spb "github.com/google/go-sev-guest/proto/sevsnp" "github.com/google/go-sev-guest/verify/trust" + "github.com/google/go-tpm-tools/proto/attest" ) // Product returns the SEV product info currently supported by Constellation's SNP attestation. @@ -39,6 +40,7 @@ type InstanceInfo struct { // AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM. AttestationReport []byte Azure *AzureInstanceInfo + GCP *attest.GCEInstanceInfo } // AzureInstanceInfo contains Azure specific information related to SNP attestation. diff --git a/internal/attestation/vtpm/attestation.go b/internal/attestation/vtpm/attestation.go index 77c396b9a4..f0e233f5d3 100644 --- a/internal/attestation/vtpm/attestation.go +++ b/internal/attestation/vtpm/attestation.go @@ -9,10 +9,12 @@ package vtpm import ( "context" "crypto" + "crypto/sha256" "encoding/json" "errors" "fmt" "io" + "slices" "github.com/google/go-sev-guest/proto/sevsnp" tpmClient "github.com/google/go-tpm-tools/client" @@ -125,10 +127,6 @@ func (i *Issuer) Issue(ctx context.Context, userData []byte, nonce []byte) (res // Create an attestation using the loaded key extraData := attestation.MakeExtraData(userData, nonce) - tpmAttestation, err := aK.Attest(tpmClient.AttestOpts{Nonce: extraData}) - if err != nil { - return nil, fmt.Errorf("creating attestation: %w", err) - } // Fetch instance info of the VM instanceInfo, err := i.getInstanceInfo(ctx, tpm, extraData) @@ -136,6 +134,13 @@ func (i *Issuer) Issue(ctx context.Context, userData []byte, nonce []byte) (res return nil, fmt.Errorf("fetching instance info: %w", err) } + tpmNonce := makeTpmNonce(instanceInfo, extraData) + + tpmAttestation, err := aK.Attest(tpmClient.AttestOpts{Nonce: tpmNonce[:]}) + if err != nil { + return nil, fmt.Errorf("creating attestation: %w", err) + } + attDoc := AttestationDocument{ Attestation: tpmAttestation, InstanceInfo: instanceInfo, @@ -208,11 +213,13 @@ func (v *Validator) Validate(ctx context.Context, attDocRaw []byte, nonce []byte return nil, fmt.Errorf("validating attestation public key: %w", err) } + tpmNonce := makeTpmNonce(attDoc.InstanceInfo, extraData) + // Verify the TPM attestation state, err := tpmServer.VerifyAttestation( attDoc.Attestation, tpmServer.VerifyOpts{ - Nonce: extraData, + Nonce: tpmNonce[:], TrustedAKs: []crypto.PublicKey{aKP}, AllowSHA1: false, }, @@ -287,3 +294,9 @@ func GetSelectedMeasurements(open TPMOpenFunc, selection tpm2.PCRSelection) (mea return m, nil } + +// makeTpmNonce creates a nonce for the TPM attestation and returns it in its marshaled form. +func makeTpmNonce(instanceInfo []byte, extraData []byte) [32]byte { + // Finding: GCP nonces cannot be larger than 32 bytes. + return sha256.Sum256(slices.Concat(instanceInfo, extraData)) +} From 57498a3f780b671f73366553596411efeb6a738f Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:34:10 +0200 Subject: [PATCH 07/30] gcp: factor out common logic --- internal/attestation/gcp/BUILD.bazel | 28 +------ internal/attestation/gcp/gcp.go | 37 +-------- internal/attestation/gcp/metadata.go | 66 ++++++++++++++++ internal/attestation/gcp/restclient.go | 101 +++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 61 deletions(-) create mode 100644 internal/attestation/gcp/metadata.go create mode 100644 internal/attestation/gcp/restclient.go diff --git a/internal/attestation/gcp/BUILD.bazel b/internal/attestation/gcp/BUILD.bazel index 7cabb294b2..8b8c24d8ca 100644 --- a/internal/attestation/gcp/BUILD.bazel +++ b/internal/attestation/gcp/BUILD.bazel @@ -1,21 +1,18 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//bazel/go:go_test.bzl", "go_test") go_library( name = "gcp", srcs = [ "gcp.go", - "issuer.go", - "validator.go", + "metadata.go", + "restclient.go", ], importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/gcp", visibility = ["//:__subpackages__"], deps = [ - "//internal/attestation", + "//internal/attestation/snp", "//internal/attestation/variant", "//internal/attestation/vtpm", - "//internal/config", - "@com_github_google_go_tpm_tools//client", "@com_github_google_go_tpm_tools//proto/attest", "@com_github_googleapis_gax_go_v2//:gax-go", "@com_google_cloud_go_compute//apiv1", @@ -24,22 +21,3 @@ go_library( "@org_golang_google_api//option", ], ) - -go_test( - name = "gcp_test", - srcs = [ - "issuer_test.go", - "validator_test.go", - ], - embed = [":gcp"], - deps = [ - "//internal/attestation/vtpm", - "@com_github_google_go_tpm_tools//proto/attest", - "@com_github_googleapis_gax_go_v2//:gax-go", - "@com_github_stretchr_testify//assert", - "@com_github_stretchr_testify//require", - "@com_google_cloud_go_compute//apiv1/computepb", - "@org_golang_google_api//option", - "@org_golang_google_protobuf//proto", - ], -) diff --git a/internal/attestation/gcp/gcp.go b/internal/attestation/gcp/gcp.go index 893b002a69..b9a9a4ac58 100644 --- a/internal/attestation/gcp/gcp.go +++ b/internal/attestation/gcp/gcp.go @@ -5,41 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only */ /* -# Google Cloud Platform attestation - -Google offers [confidential VMs], utilizing AMD SEV-ES to provide memory encryption. - -AMD SEV-ES doesn't offer much in terms of remote attestation, and following that the VMs don't offer much either, see [their docs] on how to validate a confidential VM for some insights. -However, each VM comes with a [virtual Trusted Platform Module (vTPM)]. -This module can be used to generate VM unique encryption keys or to attest the platform's chain of boot. We can use the vTPM to verify the VM is running on AMD SEV-ES enabled hardware, allowing us to bootstrap a constellation cluster. - -# Issuer - -Generates a TPM attestation key using a Google provided attestation key. -Additionally project ID, zone, and instance name are fetched from the metadata server and attached to the attestation document. - -# Validator - -Verifies the TPM attestation by using a public key provided by Google's API corresponding to the project ID, zone, instance name tuple attached to the attestation document. - -# Problems - - - SEV-ES is somewhat limited when compared to the newer version SEV-SNP - - Comparison of SEV, SEV-ES, and SEV-SNP can be seen on page seven of [AMD's SNP whitepaper] - - - We have to trust Google - - Since the vTPM is provided by Google, and they could do whatever they want with it, we have no save proof of the VMs actually being confidential. - - - The provided vTPM has no endorsement certificate for its attestation key - - Without a certificate signing the authenticity of any endorsement keys we have no way of establishing a chain of trust. - Instead, we have to rely on Google's API to provide us with the public key of the vTPM's endorsement key. - -[confidential VMs]: https://cloud.google.com/compute/confidential-vm/docs/about-cvm -[their docs]: https://cloud.google.com/compute/confidential-vm/docs/monitoring -[virtual Trusted Platform Module (vTPM)]: https://cloud.google.com/security/shielded-cloud/shielded-vm#vtpm -[AMD's SNP whitepaper]: https://www.amd.com/system/files/TechDocs/SEV-SNP-strengthening-vm-isolation-with-integrity-protection-and-more.pdf#page=7 +# Google Cloud Platform Attestation */ package gcp diff --git a/internal/attestation/gcp/metadata.go b/internal/attestation/gcp/metadata.go new file mode 100644 index 0000000000..a2264d719b --- /dev/null +++ b/internal/attestation/gcp/metadata.go @@ -0,0 +1,66 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package gcp + +import ( + "context" + "encoding/json" + "errors" + "io" + + "cloud.google.com/go/compute/metadata" + "github.com/google/go-tpm-tools/proto/attest" +) + +// GCEInstanceInfo fetches VM metadata used for attestation from the GCE Metadata API. +func GCEInstanceInfo(client gcpMetadataClient) func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) { + // Ideally we would want to use the endorsement public key certificate + // However, this is not available on GCE instances + // Workaround: Provide ShieldedVM instance info + // The attestating party can request the VMs signing key using Google's API + return func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) { + projectID, err := client.ProjectID() + if err != nil { + return nil, errors.New("unable to fetch projectID") + } + zone, err := client.Zone() + if err != nil { + return nil, errors.New("unable to fetch zone") + } + instanceName, err := client.InstanceName() + if err != nil { + return nil, errors.New("unable to fetch instance name") + } + + return json.Marshal(&attest.GCEInstanceInfo{ + Zone: zone, + ProjectId: projectID, + InstanceName: instanceName, + }) + } +} + +type gcpMetadataClient interface { + ProjectID() (string, error) + InstanceName() (string, error) + Zone() (string, error) +} + +// a MetadataClient fetches metadata from the GCE Metadata API. +type MetadataClient struct{} + +func (c MetadataClient) ProjectID() (string, error) { + return metadata.ProjectID() +} + +func (c MetadataClient) InstanceName() (string, error) { + return metadata.InstanceName() +} + +func (c MetadataClient) Zone() (string, error) { + return metadata.Zone() +} diff --git a/internal/attestation/gcp/restclient.go b/internal/attestation/gcp/restclient.go new file mode 100644 index 0000000000..34b92f33ed --- /dev/null +++ b/internal/attestation/gcp/restclient.go @@ -0,0 +1,101 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package gcp + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/google/go-tpm-tools/proto/attest" + "github.com/googleapis/gax-go/v2" + "google.golang.org/api/option" +) + +// RESTClient is a client for the GCE API. +type RESTClient struct { + *compute.InstancesClient +} + +// NewRESTClient creates a new RESTClient. +func NewRESTClient(ctx context.Context, opts ...option.ClientOption) (GCPRESTClient, error) { + c, err := compute.NewInstancesRESTClient(ctx, opts...) + if err != nil { + return nil, err + } + return &RESTClient{c}, nil +} + +// GCPRESTClient is the interface a GCP REST client must implement. +type GCPRESTClient interface { + GetShieldedInstanceIdentity(ctx context.Context, req *computepb.GetShieldedInstanceIdentityInstanceRequest, opts ...gax.CallOption) (*computepb.ShieldedInstanceIdentity, error) + Close() error +} + +// TrustedKeyGetter returns a function that queries the GCE API for a shieldedVM's public signing key. +// This key can be used to verify attestation statements issued by the VM. +func TrustedKeyGetter( + attestationVariant variant.Variant, + newRESTClient func(ctx context.Context, opts ...option.ClientOption) (GCPRESTClient, error), +) (func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error), error) { + return func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { + client, err := newRESTClient(ctx) + if err != nil { + return nil, fmt.Errorf("creating GCE client: %w", err) + } + defer client.Close() + + var gceInstanceInfo attest.GCEInstanceInfo + switch attestationVariant { + case variant.GCPSEVES{}: + if err := json.Unmarshal(attDoc.InstanceInfo, &gceInstanceInfo); err != nil { + return nil, err + } + case variant.GCPSEVSNP{}: + var instanceInfo snp.InstanceInfo + if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil { + return nil, err + } + gceInstanceInfo = attest.GCEInstanceInfo{ + InstanceName: instanceInfo.GCP.InstanceName, + ProjectId: instanceInfo.GCP.ProjectId, + Zone: instanceInfo.GCP.Zone, + } + default: + return nil, fmt.Errorf("unsupported attestation variant: %v", attestationVariant) + } + + instance, err := client.GetShieldedInstanceIdentity(ctx, &computepb.GetShieldedInstanceIdentityInstanceRequest{ + Instance: gceInstanceInfo.GetInstanceName(), + Project: gceInstanceInfo.GetProjectId(), + Zone: gceInstanceInfo.GetZone(), + }) + if err != nil { + return nil, fmt.Errorf("retrieving VM identity: %w", err) + } + + if instance.SigningKey == nil || instance.SigningKey.EkPub == nil { + return nil, fmt.Errorf("received no signing key from GCP API") + } + + // Parse the signing key return by GetShieldedInstanceIdentity + block, _ := pem.Decode([]byte(*instance.SigningKey.EkPub)) + if block == nil || block.Type != "PUBLIC KEY" { + return nil, fmt.Errorf("failed to decode PEM block containing public key") + } + + return x509.ParsePKIXPublicKey(block.Bytes) + }, nil +} From 68037cf36496a51c8557f51d2d827ac8a2e0ec5a Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:34:26 +0200 Subject: [PATCH 08/30] choose: add GCP SEV-SNP --- internal/attestation/choose/BUILD.bazel | 3 ++- internal/attestation/choose/choose.go | 11 ++++++++--- internal/attestation/choose/choose_test.go | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/attestation/choose/BUILD.bazel b/internal/attestation/choose/BUILD.bazel index dfb1938e42..09bd9d2b95 100644 --- a/internal/attestation/choose/BUILD.bazel +++ b/internal/attestation/choose/BUILD.bazel @@ -14,7 +14,8 @@ go_library( "//internal/attestation/azure/snp", "//internal/attestation/azure/tdx", "//internal/attestation/azure/trustedlaunch", - "//internal/attestation/gcp", + "//internal/attestation/gcp/es", + "//internal/attestation/gcp/snp", "//internal/attestation/qemu", "//internal/attestation/tdx", "//internal/attestation/variant", diff --git a/internal/attestation/choose/choose.go b/internal/attestation/choose/choose.go index 3ce936085f..7d0e480104 100644 --- a/internal/attestation/choose/choose.go +++ b/internal/attestation/choose/choose.go @@ -16,7 +16,8 @@ import ( azuresnp "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp" azuretdx "github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx" "github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch" - "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/es" + gcpsnp "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/qemu" "github.com/edgelesssys/constellation/v2/internal/attestation/tdx" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" @@ -37,7 +38,9 @@ func Issuer(attestationVariant variant.Variant, log attestation.Logger) (atls.Is case variant.AzureTDX{}: return azuretdx.NewIssuer(log), nil case variant.GCPSEVES{}: - return gcp.NewIssuer(log), nil + return es.NewIssuer(log), nil + case variant.GCPSEVSNP{}: + return gcpsnp.NewIssuer(log), nil case variant.QEMUVTPM{}: return qemu.NewIssuer(log), nil case variant.QEMUTDX{}: @@ -63,7 +66,9 @@ func Validator(cfg config.AttestationCfg, log attestation.Logger) (atls.Validato case *config.AzureTDX: return azuretdx.NewValidator(cfg, log), nil case *config.GCPSEVES: - return gcp.NewValidator(cfg, log), nil + return es.NewValidator(cfg, log) + case *config.GCPSEVSNP: + return gcpsnp.NewValidator(cfg, log) case *config.QEMUVTPM: return qemu.NewValidator(cfg, log), nil case *config.QEMUTDX: diff --git a/internal/attestation/choose/choose_test.go b/internal/attestation/choose/choose_test.go index 33ca1849e7..31454d2c92 100644 --- a/internal/attestation/choose/choose_test.go +++ b/internal/attestation/choose/choose_test.go @@ -40,6 +40,9 @@ func TestIssuer(t *testing.T) { "gcp-sev-es": { variant: variant.GCPSEVES{}, }, + "gcp-sev-snp": { + variant: variant.GCPSEVSNP{}, + }, "qemu-vtpm": { variant: variant.QEMUVTPM{}, }, @@ -89,6 +92,9 @@ func TestValidator(t *testing.T) { "gcp-sev-es": { cfg: &config.GCPSEVES{}, }, + "gcp-sev-snp": { + cfg: &config.GCPSEVSNP{}, + }, "qemu-vtpm": { cfg: &config.QEMUVTPM{}, }, From 4298fcfb8e7b5db9725a1fdb33a4fac8d417aa0f Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:34:52 +0200 Subject: [PATCH 09/30] cli: add TF variable passthrough for GCP SEV-SNP variables --- cli/internal/terraform/variables.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/internal/terraform/variables.go b/cli/internal/terraform/variables.go index a838182600..f258a2d924 100644 --- a/cli/internal/terraform/variables.go +++ b/cli/internal/terraform/variables.go @@ -136,6 +136,8 @@ type GCPClusterVariables struct { CustomEndpoint string `hcl:"custom_endpoint" cty:"custom_endpoint"` // InternalLoadBalancer is true if an internal load balancer should be created. InternalLoadBalancer bool `hcl:"internal_load_balancer" cty:"internal_load_balancer"` + // CCTechnology is the confidential computing technology to use on the VMs. (`SEV` or `SEV_SNP`) + CCTechnology string `hcl:"cc_technology" cty:"cc_technology"` } // GetCreateMAA gets the CreateMAA variable. From 640f6c402b4e402f36adb4e39a884ea320305353 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:35:24 +0200 Subject: [PATCH 10/30] cli: support GCP SEV-SNP for `constellation verify` --- cli/internal/cmd/verify.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index f80d1128da..5b79f9346f 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -467,8 +467,7 @@ func updateInitMeasurements(config config.AttestationCfg, ownerID, clusterID str switch config.GetVariant() { case variant.AWSNitroTPM{}, variant.AWSSEVSNP{}, variant.AzureTrustedLaunch{}, variant.AzureSEVSNP{}, variant.AzureTDX{}, // AzureTDX also uses a vTPM for measurements - variant.GCPSEVES{}, - variant.QEMUVTPM{}: + variant.GCPSEVES{}, variant.GCPSEVSNP{}, variant.QEMUVTPM{}: if err := updateMeasurementTPM(m, uint32(measurements.PCRIndexOwnerID), ownerID); err != nil { return err } From cb543c75f836c3c06488d6c7aa0eff97d8e5e181 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:35:36 +0200 Subject: [PATCH 11/30] Adjust usage of GCP SEV-SNP throughout codebase --- .github/actions/terraform_apply/action.yml | 3 +++ cli/internal/cloudcmd/tfvars.go | 7 +++++++ cli/internal/cmd/configgenerate_test.go | 9 +++++++++ cli/internal/terraform/variables_test.go | 2 ++ docs/docs/reference/cli.md | 2 +- internal/constellation/state/state.go | 4 ++-- .../internal/provider/convert.go | 11 +++++++++++ 7 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/actions/terraform_apply/action.yml b/.github/actions/terraform_apply/action.yml index f66b18ace0..89361d14f4 100644 --- a/.github/actions/terraform_apply/action.yml +++ b/.github/actions/terraform_apply/action.yml @@ -26,6 +26,9 @@ runs: "gcpSEVES") attestationVariant="gcp-sev-es" ;; + "gcpSEVSNP") + attestationVariant="gcp-sev-snp" + ;; *) echo "Unknown attestation variant: $(yq '.attestation | keys | .[0]' constellation-conf.yaml)" exit 1 diff --git a/cli/internal/cloudcmd/tfvars.go b/cli/internal/cloudcmd/tfvars.go index 309632d984..818590599b 100644 --- a/cli/internal/cloudcmd/tfvars.go +++ b/cli/internal/cloudcmd/tfvars.go @@ -209,6 +209,12 @@ func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPCluste DiskType: group.StateDiskType, } } + + ccTech := "SEV" + if conf.GetAttestationConfig().GetVariant().Equal(variant.GCPSEVSNP{}) { + ccTech = "SEV_SNP" + } + return &terraform.GCPClusterVariables{ Name: conf.Name, NodeGroups: nodeGroups, @@ -219,6 +225,7 @@ func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPCluste Debug: conf.IsDebugCluster(), CustomEndpoint: conf.CustomEndpoint, InternalLoadBalancer: conf.InternalLoadBalancer, + CCTechnology: ccTech, } } diff --git a/cli/internal/cmd/configgenerate_test.go b/cli/internal/cmd/configgenerate_test.go index d1a4fbc921..952c43f8fc 100644 --- a/cli/internal/cmd/configgenerate_test.go +++ b/cli/internal/cmd/configgenerate_test.go @@ -235,6 +235,11 @@ func TestValidProviderAttestationCombination(t *testing.T) { variant.GCPSEVES{}, config.AttestationConfig{GCPSEVES: defaultAttestation.GCPSEVES}, }, + { + cloudprovider.GCP, + variant.GCPSEVSNP{}, + config.AttestationConfig{GCPSEVSNP: defaultAttestation.GCPSEVSNP}, + }, { cloudprovider.QEMU, variant.QEMUVTPM{}, @@ -286,6 +291,10 @@ func TestParseAttestationFlag(t *testing.T) { attestationFlag: "gcp-sev-es", wantVariant: variant.GCPSEVES{}, }, + "GCPSEVSNP": { + attestationFlag: "gcp-sev-snp", + wantVariant: variant.GCPSEVSNP{}, + }, "QEMUVTPM": { attestationFlag: "qemu-vtpm", wantVariant: variant.QEMUVTPM{}, diff --git a/cli/internal/terraform/variables_test.go b/cli/internal/terraform/variables_test.go index df27ddb59d..1c0ccb76b2 100644 --- a/cli/internal/terraform/variables_test.go +++ b/cli/internal/terraform/variables_test.go @@ -122,6 +122,7 @@ func TestGCPClusterVariables(t *testing.T) { }, }, CustomEndpoint: "example.com", + CCTechnology: "SEV_SNP", } // test that the variables are correctly rendered @@ -151,6 +152,7 @@ node_groups = { } custom_endpoint = "example.com" internal_load_balancer = false +cc_technology = "SEV_SNP" ` got := vars.String() assert.Equal(t, strings.Fields(want), strings.Fields(got)) // to ignore whitespace differences diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index 52391f3d14..3ed16680a8 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -78,7 +78,7 @@ constellation config generate {aws|azure|gcp|openstack|qemu|stackit} [flags] ### Options ``` - -a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-tdx|azure-trustedlaunch|gcp-sev-es|qemu-vtpm}. If not specified, the default for the cloud provider is used + -a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-tdx|azure-trustedlaunch|gcp-sev-snp|gcp-sev-es|qemu-vtpm}. If not specified, the default for the cloud provider is used -h, --help help for generate -k, --kubernetes string Kubernetes version to use in format MAJOR.MINOR (default "v1.28") ``` diff --git a/internal/constellation/state/state.go b/internal/constellation/state/state.go index bee5f8b2b7..68e9b2845b 100644 --- a/internal/constellation/state/state.go +++ b/internal/constellation/state/state.go @@ -383,7 +383,7 @@ func (s *State) preInitConstraints(attestation variant.Variant) func() []*valida ), ) } - case variant.GCPSEVES{}: + case variant.GCPSEVES{}, variant.GCPSEVSNP{}: // GCP values need to be valid after infrastructure creation. constraints = append(constraints, // Azure values need to be nil or empty. @@ -514,7 +514,7 @@ func (s *State) postInitConstraints(attestation variant.Variant) func() []*valid ), ) } - case variant.GCPSEVES{}: + case variant.GCPSEVES{}, variant.GCPSEVSNP{}: constraints = append(constraints, // Azure values need to be nil or empty. validation.Or( diff --git a/terraform-provider-constellation/internal/provider/convert.go b/terraform-provider-constellation/internal/provider/convert.go index 0877281683..cfe9ec7fab 100644 --- a/terraform-provider-constellation/internal/provider/convert.go +++ b/terraform-provider-constellation/internal/provider/convert.go @@ -122,6 +122,10 @@ func convertFromTfAttestationCfg(tfAttestation attestationAttribute, attestation attestationConfig = &config.GCPSEVES{ Measurements: c11nMeasurements, } + case variant.GCPSEVSNP{}: + attestationConfig = &config.GCPSEVSNP{ + Measurements: c11nMeasurements, + } case variant.QEMUVTPM{}: attestationConfig = &config.QEMUVTPM{ Measurements: c11nMeasurements, @@ -150,6 +154,13 @@ func convertToTfAttestation(attVar variant.Variant, snpVersions attestationconfi } tfAttestation.AMDRootKey = certStr + case variant.GCPSEVSNP{}: + certStr, err := certAsString(config.DefaultForGCPSEVSNP().AMDRootKey) + if err != nil { + return tfAttestation, err + } + tfAttestation.AMDRootKey = certStr + case variant.AzureSEVSNP{}: certStr, err := certAsString(config.DefaultForAzureSEVSNP().AMDRootKey) if err != nil { From 25e1eca26d99302b4520ed4f66bb1c59e9d19048 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:44:08 +0200 Subject: [PATCH 12/30] ci: add GCP SEV-SNP --- .github/workflows/e2e-test-daily.yml | 2 +- .github/workflows/e2e-test-internal-lb.yml | 5 ++- .../workflows/e2e-test-marketplace-image.yml | 5 ++- .../workflows/e2e-test-provider-example.yml | 1 + .github/workflows/e2e-test-release.yml | 44 +++++++++++++++++++ .../workflows/e2e-test-terraform-provider.yml | 5 ++- .github/workflows/e2e-test-weekly.yml | 40 +++++++++++++++++ .github/workflows/e2e-test.yml | 1 + .github/workflows/e2e-upgrade.yml | 5 ++- .github/workflows/on-release.yml | 1 + 10 files changed, 100 insertions(+), 9 deletions(-) diff --git a/.github/workflows/e2e-test-daily.yml b/.github/workflows/e2e-test-daily.yml index c36923a971..c2a4880ed4 100644 --- a/.github/workflows/e2e-test-daily.yml +++ b/.github/workflows/e2e-test-daily.yml @@ -46,7 +46,7 @@ jobs: max-parallel: 5 matrix: kubernetesVersion: ["1.28"] # should be default - attestationVariant: ["gcp-sev-es", "azure-sev-snp", "azure-tdx", "aws-sev-snp"] + attestationVariant: ["gcp-sev-es", "gcp-sev-snp", "azure-sev-snp", "azure-tdx", "aws-sev-snp"] refStream: ["ref/main/stream/debug/?", "ref/release/stream/stable/?"] test: ["sonobuoy quick"] runs-on: ubuntu-22.04 diff --git a/.github/workflows/e2e-test-internal-lb.yml b/.github/workflows/e2e-test-internal-lb.yml index 6e87bd30da..b9a27949c3 100644 --- a/.github/workflows/e2e-test-internal-lb.yml +++ b/.github/workflows/e2e-test-internal-lb.yml @@ -11,10 +11,11 @@ on: description: "Which attestation variant to use." type: choice options: - - "gcp-sev-es" + - "aws-sev-snp" - "azure-sev-snp" - "azure-tdx" - - "aws-sev-snp" + - "gcp-sev-es" + - "gcp-sev-snp" default: "azure-sev-snp" required: true runner: diff --git a/.github/workflows/e2e-test-marketplace-image.yml b/.github/workflows/e2e-test-marketplace-image.yml index 94e790cbbd..3338c13847 100644 --- a/.github/workflows/e2e-test-marketplace-image.yml +++ b/.github/workflows/e2e-test-marketplace-image.yml @@ -11,10 +11,11 @@ on: description: "Which attestation variant to use." type: choice options: - - "gcp-sev-es" + - "aws-sev-snp" - "azure-sev-snp" - "azure-tdx" - - "aws-sev-snp" + - "gcp-sev-es" + - "gcp-sev-snp" default: "azure-sev-snp" required: true runner: diff --git a/.github/workflows/e2e-test-provider-example.yml b/.github/workflows/e2e-test-provider-example.yml index 91a807e593..a011953e3e 100644 --- a/.github/workflows/e2e-test-provider-example.yml +++ b/.github/workflows/e2e-test-provider-example.yml @@ -31,6 +31,7 @@ on: - "azure-sev-snp" - "azure-tdx" - "gcp-sev-es" + - "gcp-sev-snp" default: "azure-sev-snp" required: true workflow_call: diff --git a/.github/workflows/e2e-test-release.yml b/.github/workflows/e2e-test-release.yml index 7bc6deb92b..4843d2cb7f 100644 --- a/.github/workflows/e2e-test-release.yml +++ b/.github/workflows/e2e-test-release.yml @@ -49,6 +49,10 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" runner: "ubuntu-22.04" + - test: "sonobuoy full" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + runner: "ubuntu-22.04" clusterCreation: "cli" - test: "sonobuoy full" attestationVariant: "azure-sev-snp" @@ -72,6 +76,11 @@ jobs: kubernetes-version: "v1.28" runner: "ubuntu-22.04" clusterCreation: "cli" + - test: "sonobuoy full" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.28" + runner: "ubuntu-22.04" + clusterCreation: "cli" - test: "sonobuoy full" attestationVariant: "azure-sev-snp" kubernetes-version: "v1.28" @@ -93,6 +102,11 @@ jobs: kubernetes-version: "v1.27" runner: "ubuntu-22.04" clusterCreation: "cli" + - test: "sonobuoy full" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.27" + runner: "ubuntu-22.04" + clusterCreation: "cli" - test: "sonobuoy full" attestationVariant: "azure-sev-snp" kubernetes-version: "v1.27" @@ -115,6 +129,11 @@ jobs: kubernetes-version: "v1.29" runner: "ubuntu-22.04" clusterCreation: "cli" + - test: "verify" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + runner: "ubuntu-22.04" + clusterCreation: "cli" - test: "verify" attestationVariant: "azure-sev-snp" kubernetes-version: "v1.29" @@ -137,6 +156,11 @@ jobs: kubernetes-version: "v1.29" runner: "ubuntu-22.04" clusterCreation: "cli" + - test: "recover" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + runner: "ubuntu-22.04" + clusterCreation: "cli" - test: "recover" attestationVariant: "azure-sev-snp" kubernetes-version: "v1.29" @@ -159,6 +183,11 @@ jobs: kubernetes-version: "v1.29" runner: "ubuntu-22.04" clusterCreation: "cli" + - test: "lb" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + runner: "ubuntu-22.04" + clusterCreation: "cli" - test: "lb" attestationVariant: "azure-sev-snp" kubernetes-version: "v1.29" @@ -181,6 +210,11 @@ jobs: kubernetes-version: "v1.29" runner: "ubuntu-22.04" clusterCreation: "cli" + - test: "autoscaling" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + runner: "ubuntu-22.04" + clusterCreation: "cli" - test: "autoscaling" attestationVariant: "azure-sev-snp" kubernetes-version: "v1.29" @@ -203,6 +237,11 @@ jobs: kubernetes-version: "v1.29" runner: "ubuntu-22.04" clusterCreation: "cli" + - test: "perf-bench" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + runner: "ubuntu-22.04" + clusterCreation: "cli" - test: "perf-bench" attestationVariant: "azure-sev-snp" kubernetes-version: "v1.29" @@ -223,6 +262,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" clusterCreation: "cli" + - test: "malicious join" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + clusterCreation: "cli" - test: "malicious join" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" diff --git a/.github/workflows/e2e-test-terraform-provider.yml b/.github/workflows/e2e-test-terraform-provider.yml index f62b204d1c..585ffe6b90 100644 --- a/.github/workflows/e2e-test-terraform-provider.yml +++ b/.github/workflows/e2e-test-terraform-provider.yml @@ -11,10 +11,11 @@ on: description: "Which attestation variant to use." type: choice options: - - "gcp-sev-es" + - "aws-sev-snp" - "azure-sev-snp" - "azure-tdx" - - "aws-sev-snp" + - "gcp-sev-es" + - "gcp-sev-snp" default: "azure-sev-snp" required: true runner: diff --git a/.github/workflows/e2e-test-weekly.yml b/.github/workflows/e2e-test-weekly.yml index 79e321beae..d0a32c4ae5 100644 --- a/.github/workflows/e2e-test-weekly.yml +++ b/.github/workflows/e2e-test-weekly.yml @@ -57,6 +57,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" clusterCreation: "cli" + - test: "sonobuoy full" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + clusterCreation: "cli" - test: "sonobuoy full" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" @@ -79,6 +84,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.28" clusterCreation: "cli" + - test: "sonobuoy quick" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.28" + clusterCreation: "cli" - test: "sonobuoy quick" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" @@ -100,6 +110,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.27" clusterCreation: "cli" + - test: "sonobuoy quick" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.27" + clusterCreation: "cli" - test: "sonobuoy quick" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" @@ -123,6 +138,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" clusterCreation: "cli" + - test: "verify" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + clusterCreation: "cli" - test: "verify" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" @@ -146,6 +166,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" clusterCreation: "cli" + - test: "recover" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + clusterCreation: "cli" - test: "recover" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" @@ -168,6 +193,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" clusterCreation: "cli" + - test: "lb" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + clusterCreation: "cli" - test: "lb" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" @@ -190,6 +220,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" clusterCreation: "cli" + - test: "autoscaling" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + clusterCreation: "cli" - test: "autoscaling" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" @@ -212,6 +247,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.29" clusterCreation: "cli" + - test: "perf-bench" + refStream: "ref/main/stream/debug/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.29" + clusterCreation: "cli" - test: "perf-bench" refStream: "ref/main/stream/debug/?" attestationVariant: "azure-sev-snp" diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 1d5fcfdcac..61c05349a5 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -12,6 +12,7 @@ on: type: choice options: - "gcp-sev-es" + - "gcp-sev-snp" - "azure-sev-snp" - "azure-tdx" - "aws-sev-snp" diff --git a/.github/workflows/e2e-upgrade.yml b/.github/workflows/e2e-upgrade.yml index c035ac4921..34ff34a253 100644 --- a/.github/workflows/e2e-upgrade.yml +++ b/.github/workflows/e2e-upgrade.yml @@ -7,10 +7,11 @@ on: description: "Which attestation variant to use." type: choice options: - - "gcp-sev-es" + - "aws-sev-snp" - "azure-sev-snp" - "azure-tdx" - - "aws-sev-snp" + - "gcp-sev-es" + - "gcp-sev-snp" default: "azure-sev-snp" required: true nodeCount: diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml index f2853a7a84..e1c8513d03 100644 --- a/.github/workflows/on-release.yml +++ b/.github/workflows/on-release.yml @@ -161,6 +161,7 @@ jobs: id: fetch-reference shell: bash run: | + # TODO(msanft): Implement marketplace images for GCP SEV-SNP aws s3 cp s3://cdn-constellation-backend/constellation/v2/ref/-/stream/stable/${{ steps.fetch-version.outputs.output }}/image/info.json . FULL_REF=$(yq e -r -oy '.list.[] | select(.attestationVariant == "gcp-sev-es") | .reference' info.json) IMAGE_NAME=$(echo "${FULL_REF}" | cut -d / -f 5) From b7fe2efa92ad6d14ab5f0834defebccfc134c972 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:51:08 +0200 Subject: [PATCH 13/30] terraform-provider: support GCP SEV-SNP --- .../provider/attestation_data_source_test.go | 29 ++++++++++++++++++- .../provider/image_data_source_test.go | 19 +++++++++++- .../internal/provider/shared_attributes.go | 3 +- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/terraform-provider-constellation/internal/provider/attestation_data_source_test.go b/terraform-provider-constellation/internal/provider/attestation_data_source_test.go index 4fed9fbe32..ddb7ed7a85 100644 --- a/terraform-provider-constellation/internal/provider/attestation_data_source_test.go +++ b/terraform-provider-constellation/internal/provider/attestation_data_source_test.go @@ -84,7 +84,7 @@ func TestAccAttestationSource(t *testing.T) { }, }, }, - "gcp sev-snp succcess": { + "gcp sev-es succcess": { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, PreCheck: bazelPreCheck, Steps: []resource.TestStep{ @@ -110,6 +110,33 @@ func TestAccAttestationSource(t *testing.T) { }, }, }, + // TODO(msanft): Enable once v2.17.0 is available + // "gcp sev-snp succcess": { + // ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + // PreCheck: bazelPreCheck, + // Steps: []resource.TestStep{ + // { + // Config: testingConfig + ` + // data "constellation_attestation" "test" { + // csp = "gcp" + // attestation_variant = "gcp-sev-snp" + // image = { + // version = "v2.17.0" + // reference = "v2.17.0" + // short_path = "v2.17.0" + // } + // } + // `, + // Check: resource.ComposeAggregateTestCheckFunc( + // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.variant", "gcp-sev-es"), + // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.bootloader_version", "0"), // since this is not supported on GCP, we expect 0 + + // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.measurements.1.expected", "745f2fb4235e4647aa0ad5ace781cd929eb68c28870e7dd5d1a1535854325e56"), + // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.measurements.1.warn_only", "true"), + // ), + // }, + // }, + // }, "STACKIT qemu-vtpm success": { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, PreCheck: bazelPreCheck, diff --git a/terraform-provider-constellation/internal/provider/image_data_source_test.go b/terraform-provider-constellation/internal/provider/image_data_source_test.go index 669899e396..986ee1b534 100644 --- a/terraform-provider-constellation/internal/provider/image_data_source_test.go +++ b/terraform-provider-constellation/internal/provider/image_data_source_test.go @@ -125,7 +125,7 @@ func TestAccImageDataSource(t *testing.T) { }, }, }, - "gcp success": { + "gcp sev-es success": { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, PreCheck: bazelPreCheck, Steps: []resource.TestStep{ @@ -141,6 +141,23 @@ func TestAccImageDataSource(t *testing.T) { }, }, }, + // TODO(msanft): Enable once v2.17.0 is available + // "gcp sev-snp success": { + // ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + // PreCheck: bazelPreCheck, + // Steps: []resource.TestStep{ + // { + // Config: testingConfig + ` + // data "constellation_image" "test" { + // version = "v2.17.0" + // attestation_variant = "gcp-sev-snp" + // csp = "gcp" + // } + // `, + // Check: resource.TestCheckResourceAttr("data.constellation_image.test", "image.reference", "projects/constellation-images/global/images/v2-13-0-gcp-sev-es-stable"), // should be immutable, + // }, + // }, + // }, "stackit success": { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, PreCheck: bazelPreCheck, diff --git a/terraform-provider-constellation/internal/provider/shared_attributes.go b/terraform-provider-constellation/internal/provider/shared_attributes.go index b6f96cd170..0a8c6a72d3 100644 --- a/terraform-provider-constellation/internal/provider/shared_attributes.go +++ b/terraform-provider-constellation/internal/provider/shared_attributes.go @@ -32,11 +32,12 @@ func newAttestationVariantAttributeSchema(t attributeType) schema.Attribute { " * `azure-sev-snp`\n" + " * `azure-tdx`\n" + " * `gcp-sev-es`\n" + + " * `gcp-sev-snp`\n" + " * `qemu-vtpm`\n", Required: isInput, Computed: !isInput, Validators: []validator.String{ - stringvalidator.OneOf("aws-sev-snp", "aws-nitro-tpm", "azure-sev-snp", "azure-tdx", "gcp-sev-es", "qemu-vtpm"), + stringvalidator.OneOf("aws-sev-snp", "aws-nitro-tpm", "azure-sev-snp", "azure-tdx", "gcp-sev-es", "gcp-sev-snp", "qemu-vtpm"), }, } } From 2ecffaf601a4031827ae4a465f7630bc2ac9b1ab Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:55:26 +0200 Subject: [PATCH 14/30] docs: add GCP SEV-SNP reference --- docs/docs/overview/clouds.md | 2 +- docs/docs/reference/cli.md | 2 +- .../docs/data-sources/attestation.md | 2 ++ terraform-provider-constellation/docs/data-sources/image.md | 1 + terraform-provider-constellation/docs/resources/cluster.md | 1 + 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/docs/overview/clouds.md b/docs/docs/overview/clouds.md index a7b1361e86..4f6b60187d 100644 --- a/docs/docs/overview/clouds.md +++ b/docs/docs/overview/clouds.md @@ -5,7 +5,7 @@ What works on which cloud? Currently, Confidential VMs (CVMs) are available in v For Constellation, the ideal environment provides the following: 1. Ability to run arbitrary software and images inside CVMs -2. CVMs based on AMD SEV-SNP (available in EPYC CPUs since the Milan generation) or Intel TDX (available in Xeon CPUs since the Sapphire Rapids generation) +2. CVMs based on AMD SEV-SNP (available in EPYC CPUs since the Milan generation) or Intel TDX (available in Xeon CPUs since the Sapphire Rapid generation) 3. Ability for CVM guests to obtain raw hardware attestation statements 4. Reviewable, open-source firmware inside CVMs 5. Capability of the firmware to attest the integrity of the code it passes control to, e.g., with an embedded virtual TPM (vTPM) diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index 3ed16680a8..f536ea9149 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -78,7 +78,7 @@ constellation config generate {aws|azure|gcp|openstack|qemu|stackit} [flags] ### Options ``` - -a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-tdx|azure-trustedlaunch|gcp-sev-snp|gcp-sev-es|qemu-vtpm}. If not specified, the default for the cloud provider is used + -a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-tdx|azure-trustedlaunch|gcp-sev-es|gcp-sev-snp|qemu-vtpm}. If not specified, the default for the cloud provider is used -h, --help help for generate -k, --kubernetes string Kubernetes version to use in format MAJOR.MINOR (default "v1.28") ``` diff --git a/terraform-provider-constellation/docs/data-sources/attestation.md b/terraform-provider-constellation/docs/data-sources/attestation.md index ec4118c0f7..b1b8891c01 100644 --- a/terraform-provider-constellation/docs/data-sources/attestation.md +++ b/terraform-provider-constellation/docs/data-sources/attestation.md @@ -33,6 +33,7 @@ data "constellation_attestation" "test" { * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` - `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`) See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports. @@ -83,6 +84,7 @@ Read-Only: * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` diff --git a/terraform-provider-constellation/docs/data-sources/image.md b/terraform-provider-constellation/docs/data-sources/image.md index 7f7186b56f..f0b37455a2 100644 --- a/terraform-provider-constellation/docs/data-sources/image.md +++ b/terraform-provider-constellation/docs/data-sources/image.md @@ -32,6 +32,7 @@ data "constellation_image" "example" { * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` - `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`) See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports. diff --git a/terraform-provider-constellation/docs/resources/cluster.md b/terraform-provider-constellation/docs/resources/cluster.md index 7b6d1ca210..cf77d1f746 100644 --- a/terraform-provider-constellation/docs/resources/cluster.md +++ b/terraform-provider-constellation/docs/resources/cluster.md @@ -111,6 +111,7 @@ Required: * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` Optional: From 3c07430e175cfb413df09d5dcd410d84d810c21e Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:02:06 +0200 Subject: [PATCH 15/30] linter fixes --- internal/attestation/gcp/es/validator_test.go | 6 ++-- internal/attestation/gcp/metadata.go | 5 ++- internal/attestation/gcp/restclient.go | 8 ++--- internal/attestation/gcp/snp/validator.go | 2 +- .../iam/gcp/.terraform.lock.hcl | 36 +++++++++---------- terraform/infrastructure/iam/gcp/main.tf | 2 +- .../legacy-module/gcp-constellation/main.tf | 1 + .../gcp-constellation/variables.tf | 9 +++++ 8 files changed, 41 insertions(+), 28 deletions(-) diff --git a/internal/attestation/gcp/es/validator_test.go b/internal/attestation/gcp/es/validator_test.go index 3eba824027..3fa35da7e5 100644 --- a/internal/attestation/gcp/es/validator_test.go +++ b/internal/attestation/gcp/es/validator_test.go @@ -89,7 +89,7 @@ Y+t5OxL3kL15VzY1Ob0d5cMCAwEAAQ== testCases := map[string]struct { instanceInfo []byte - getClient func(ctx context.Context, opts ...option.ClientOption) (gcp.GCPRESTClient, error) + getClient func(ctx context.Context, opts ...option.ClientOption) (gcp.CVMRestClient, error) wantErr bool }{ "success": { @@ -177,8 +177,8 @@ type fakeInstanceClient struct { ident *computepb.ShieldedInstanceIdentity } -func prepareFakeClient(ident *computepb.ShieldedInstanceIdentity, newErr, getIdentErr error) func(ctx context.Context, opts ...option.ClientOption) (gcp.GCPRESTClient, error) { - return func(_ context.Context, _ ...option.ClientOption) (gcp.GCPRESTClient, error) { +func prepareFakeClient(ident *computepb.ShieldedInstanceIdentity, newErr, getIdentErr error) func(ctx context.Context, opts ...option.ClientOption) (gcp.CVMRestClient, error) { + return func(_ context.Context, _ ...option.ClientOption) (gcp.CVMRestClient, error) { return &fakeInstanceClient{ getIdentErr: getIdentErr, ident: ident, diff --git a/internal/attestation/gcp/metadata.go b/internal/attestation/gcp/metadata.go index a2264d719b..5fdd7046b0 100644 --- a/internal/attestation/gcp/metadata.go +++ b/internal/attestation/gcp/metadata.go @@ -50,17 +50,20 @@ type gcpMetadataClient interface { Zone() (string, error) } -// a MetadataClient fetches metadata from the GCE Metadata API. +// A MetadataClient fetches metadata from the GCE Metadata API. type MetadataClient struct{} +// ProjectID returns the project ID of the GCE instance. func (c MetadataClient) ProjectID() (string, error) { return metadata.ProjectID() } +// InstanceName returns the instance name of the GCE instance. func (c MetadataClient) InstanceName() (string, error) { return metadata.InstanceName() } +// Zone returns the zone the GCE instance is located in. func (c MetadataClient) Zone() (string, error) { return metadata.Zone() } diff --git a/internal/attestation/gcp/restclient.go b/internal/attestation/gcp/restclient.go index 34b92f33ed..1a9c277f3e 100644 --- a/internal/attestation/gcp/restclient.go +++ b/internal/attestation/gcp/restclient.go @@ -30,7 +30,7 @@ type RESTClient struct { } // NewRESTClient creates a new RESTClient. -func NewRESTClient(ctx context.Context, opts ...option.ClientOption) (GCPRESTClient, error) { +func NewRESTClient(ctx context.Context, opts ...option.ClientOption) (CVMRestClient, error) { c, err := compute.NewInstancesRESTClient(ctx, opts...) if err != nil { return nil, err @@ -38,8 +38,8 @@ func NewRESTClient(ctx context.Context, opts ...option.ClientOption) (GCPRESTCli return &RESTClient{c}, nil } -// GCPRESTClient is the interface a GCP REST client must implement. -type GCPRESTClient interface { +// CVMRestClient is the interface a GCP REST client for a CVM must implement. +type CVMRestClient interface { GetShieldedInstanceIdentity(ctx context.Context, req *computepb.GetShieldedInstanceIdentityInstanceRequest, opts ...gax.CallOption) (*computepb.ShieldedInstanceIdentity, error) Close() error } @@ -48,7 +48,7 @@ type GCPRESTClient interface { // This key can be used to verify attestation statements issued by the VM. func TrustedKeyGetter( attestationVariant variant.Variant, - newRESTClient func(ctx context.Context, opts ...option.ClientOption) (GCPRESTClient, error), + newRESTClient func(ctx context.Context, opts ...option.ClientOption) (CVMRestClient, error), ) (func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error), error) { return func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { client, err := newRESTClient(ctx) diff --git a/internal/attestation/gcp/snp/validator.go b/internal/attestation/gcp/snp/validator.go index a2f4afeb92..32466a54fb 100644 --- a/internal/attestation/gcp/snp/validator.go +++ b/internal/attestation/gcp/snp/validator.go @@ -79,7 +79,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo } // validateCVM validates the SEV-SNP attestation document. -func (v *Validator) validateCVM(attDoc vtpm.AttestationDocument, state *attest.MachineState) error { +func (v *Validator) validateCVM(attDoc vtpm.AttestationDocument, _ *attest.MachineState) error { pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub) if err != nil { return fmt.Errorf("decoding public area: %w", err) diff --git a/terraform/infrastructure/iam/gcp/.terraform.lock.hcl b/terraform/infrastructure/iam/gcp/.terraform.lock.hcl index 2fcd905b42..3575f3cfe8 100644 --- a/terraform/infrastructure/iam/gcp/.terraform.lock.hcl +++ b/terraform/infrastructure/iam/gcp/.terraform.lock.hcl @@ -2,26 +2,26 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { - version = "5.17.0" - constraints = "5.17.0" + version = "5.23.0" + constraints = "5.23.0" hashes = [ - "h1:9DKCaGp9EFKDLWIOWI3yA/RgWTMh0EMD6+iggVXC9l0=", - "h1:JEfDiodirnMqwNaub/anXoOtWt68aEN80QtPJxg3jsc=", - "h1:TANQI64JuScQ2LTITQqz7eh1RjhYDItdbI5p1aBOtXY=", - "h1:dT3UftIyARC7YjS4yurPlNS7WJAHICDHMXSluAAvavA=", - "h1:lu84RYioCT4OxXbFBdqom4QvSPAjMkEyHPSIAxuS7oo=", - "zh:31b4d485ee66e6ff2eb1d8e476e694904447ce2b7143a2e067e4b80a84958d13", - "zh:32e86a51c4b0b29b7a18dd95616ea2976f08a4a7385e00f2bcab266217ee4320", - "zh:357f352bf04e7bc10d61d49296bf6503f31a3db0500169cb532afde7d318643e", - "zh:4b4637ca397cc771136edf7ec5578b5ab8631a8955a86d4fce3b8c40ca8c26b4", - "zh:4fe198b7427f7bf04270a5491a0352379c2b0a1caf12e206e6e224ceb085f56a", - "zh:7abb8509a61602d5ed4c801e7cd7c8299d109bc07980352251ba79880a99abab", - "zh:b1550fe08c650d8419860da1568d3f77093d269f880cad7d720d843b2a9ec545", - "zh:c91d7079646a3fdbb927085e368a16b221a23c17cf7455d5088f0c8f5da48c9f", - "zh:d367213a5f392852ef0708283df583703b2efd0b44f9e599cd055086c371cf74", - "zh:d5b557f294f4094a865afaa0611dc2e657d485b60903f12795eeedc2e1c3aa87", + "h1:2VJTKCZWQ1DaNwclFxSo27avsYwWgq/itwLZ3xKyl/o=", + "h1:4evtipODvV5s86gihS+jyk1cSW1xLn22jy8Ox8zzhAs=", + "h1:BD+iQfFcZ0OeaZI2JWDp2sLqSr+DfZtWy4yo1OVMnTI=", + "h1:my3kqg4hIpWLu2WwRewOFxBS+FXfkAIiw8xTYVPNS9M=", + "h1:xpm8QPNp2soGqIEnf4SNoZaTlQ/SbNH63BooJkSbgX0=", + "zh:18eaaa51a8b30fed61c73799b8716a9bd08ccd382bc395c63e45b9a52ed8b300", + "zh:20c71acf091a282db88473ec6f0a684ac59891713c49b2ff1cb35c1539da3121", + "zh:2e3e9ae1d3b045dcaa39053f4d1d066fa17e5b81f4ed7a5e57cc4e6e1e651900", + "zh:531d1552f251c5a0176543defa95c2cc259fc8b9359ef6fd3df404dcead555a0", + "zh:67a7800023fa09a7d87ac02231364988749663e37e2906aa89c70eecc5955ccf", + "zh:6a8076b59d2766a05ffe521cc115f3e8df7cd2ee4c6d60de4ee4636f47714f2e", + "zh:7b39fe720bb7a1f35cd0e4dfeff617338342fc2d16bb22274b42c080ff633140", + "zh:b181e04c32aa53ad78eaf6f2746ec5fd94977187ba7314ae8e9815ef6ea56532", + "zh:bf605be2f8942d5cabb8755ff0d18f243b53f1148f5f32db762667cf64bfa949", + "zh:e981988558310df5d94e56adaa76f7444d991357fe9600c46eb70fa61f4a1394", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:fdad54c5e50751cef3f39a8666ff6adbb3bd860d396d5a9a0a3526e204f60454", + "zh:f663776d79e7e5d131b4fbd68c152f2bef3e899a19c9baabe3a441e3f5e809ea", ] } diff --git a/terraform/infrastructure/iam/gcp/main.tf b/terraform/infrastructure/iam/gcp/main.tf index 899d448c94..38afbe1ca8 100644 --- a/terraform/infrastructure/iam/gcp/main.tf +++ b/terraform/infrastructure/iam/gcp/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/legacy-module/gcp-constellation/main.tf b/terraform/legacy-module/gcp-constellation/main.tf index 27c45b1ea7..3029f1fb33 100644 --- a/terraform/legacy-module/gcp-constellation/main.tf +++ b/terraform/legacy-module/gcp-constellation/main.tf @@ -41,6 +41,7 @@ module "gcp" { zone = var.zone debug = var.debug custom_endpoint = var.custom_endpoint + cc_technology = var.cc_technology } module "constellation" { diff --git a/terraform/legacy-module/gcp-constellation/variables.tf b/terraform/legacy-module/gcp-constellation/variables.tf index 92787bfd48..0087b4fba1 100644 --- a/terraform/legacy-module/gcp-constellation/variables.tf +++ b/terraform/legacy-module/gcp-constellation/variables.tf @@ -70,3 +70,12 @@ variable "internal_load_balancer" { default = false description = "Use an internal load balancer." } + +variable "cc_technology" { + type = string + description = "The confidential computing technology to use for the nodes. One of `SEV`, `SEV_SNP`." + validation { + condition = contains(["SEV", "SEV_SNP"], var.cc_technology) + error_message = "The confidential computing technology has to be 'SEV' or 'SEV_SNP'." + } +} From ea897baf399773b4278dbb86fc143473a9f74280 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:08:57 +0200 Subject: [PATCH 16/30] gcp: only run test with TPM simulator --- internal/attestation/gcp/snp/BUILD.bazel | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/attestation/gcp/snp/BUILD.bazel b/internal/attestation/gcp/snp/BUILD.bazel index 3056543f4d..cd7bf70fc1 100644 --- a/internal/attestation/gcp/snp/BUILD.bazel +++ b/internal/attestation/gcp/snp/BUILD.bazel @@ -37,6 +37,11 @@ go_test( "validator_test.go", ], embed = [":snp"], + # keep + gotags = select({ + "//bazel/settings:tpm_simulator_enabled": [], + "//conditions:default": ["disable_tpm_simulator"], + }), deps = [ "//internal/attestation", "//internal/attestation/aws/snp/testdata", From aec00b23b7dea2af0818ea59498e3f8df1c44615 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:34:37 +0200 Subject: [PATCH 17/30] gcp: remove nonsense test --- internal/attestation/gcp/snp/BUILD.bazel | 11 +- internal/attestation/gcp/snp/issuer_test.go | 44 ----- .../attestation/gcp/snp/validator_test.go | 172 ++---------------- 3 files changed, 19 insertions(+), 208 deletions(-) delete mode 100644 internal/attestation/gcp/snp/issuer_test.go diff --git a/internal/attestation/gcp/snp/BUILD.bazel b/internal/attestation/gcp/snp/BUILD.bazel index cd7bf70fc1..1193a7e036 100644 --- a/internal/attestation/gcp/snp/BUILD.bazel +++ b/internal/attestation/gcp/snp/BUILD.bazel @@ -32,10 +32,7 @@ go_library( go_test( name = "snp_test", - srcs = [ - "issuer_test.go", - "validator_test.go", - ], + srcs = ["validator_test.go"], embed = [":snp"], # keep gotags = select({ @@ -44,16 +41,10 @@ go_test( }), deps = [ "//internal/attestation", - "//internal/attestation/aws/snp/testdata", - "//internal/attestation/simulator", - "//internal/attestation/snp", "//internal/attestation/vtpm", "//internal/config", - "//internal/logger", - "@com_github_google_go_sev_guest//abi", "@com_github_google_go_sev_guest//proto/sevsnp", "@com_github_google_go_sev_guest//verify", - "@com_github_google_go_tpm_tools//client", "@com_github_google_go_tpm_tools//proto/attest", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", diff --git a/internal/attestation/gcp/snp/issuer_test.go b/internal/attestation/gcp/snp/issuer_test.go deleted file mode 100644 index 3f2f246990..0000000000 --- a/internal/attestation/gcp/snp/issuer_test.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package snp - -import ( - "os" - "testing" - - "github.com/edgelesssys/constellation/v2/internal/attestation/simulator" - tpmclient "github.com/google/go-tpm-tools/client" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetAttestationKey(t *testing.T) { - cgo := os.Getenv("CGO_ENABLED") - if cgo == "0" { - t.Skip("skipping test because CGO is disabled and tpm simulator requires it") - } - - require := require.New(t) - assert := assert.New(t) - - tpm, err := simulator.OpenSimulatedTPM() - require.NoError(err) - defer tpm.Close() - - // create the attestation key in RSA format - tpmAk, err := tpmclient.AttestationKeyRSA(tpm) - assert.NoError(err) - assert.NotNil(tpmAk) - - // get the cached, already created key - getAk, err := getAttestationKey(tpm) - assert.NoError(err) - assert.NotNil(getAk) - - // if everything worked fine, tpmAk and getAk are the same key - assert.Equal(tpmAk, getAk) -} diff --git a/internal/attestation/gcp/snp/validator_test.go b/internal/attestation/gcp/snp/validator_test.go index 5772c357ed..6c624e55a7 100644 --- a/internal/attestation/gcp/snp/validator_test.go +++ b/internal/attestation/gcp/snp/validator_test.go @@ -11,24 +11,16 @@ import ( "context" "crypto" "crypto/x509" - "encoding/base64" "encoding/hex" - "encoding/json" "encoding/pem" "errors" "fmt" - "regexp" "testing" "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/attestation/aws/snp/testdata" - "github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/constellation/v2/internal/config" - "github.com/edgelesssys/constellation/v2/internal/logger" - "github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/proto/sevsnp" - spb "github.com/google/go-sev-guest/proto/sevsnp" "github.com/google/go-sev-guest/verify" "github.com/google/go-tpm-tools/proto/attest" "github.com/stretchr/testify/assert" @@ -36,28 +28,30 @@ import ( ) func TestGetTrustedKey(t *testing.T) { - validator := func() *Validator { return &Validator{reportValidator: stubGCPValidator{}} } + validator := func(ek []byte) *Validator { + return &Validator{ + reportValidator: stubGCPValidator{}, + gceKeyGetter: func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { + return ek, nil + }, + } + } testCases := map[string]struct { - akPub []byte - info []byte - wantErr bool + akPub []byte + ek []byte + info []byte }{ - "null byte docs": { - akPub: []byte{0x00, 0x00, 0x00, 0x00}, - info: []byte{0x00, 0x00, 0x00, 0x00}, - wantErr: true, - }, - "nil": { - akPub: nil, - info: nil, - wantErr: true, + "success": { + akPub: []byte("akPub"), + ek: []byte("ek"), + info: []byte("info"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) - out, err := validator().getTrustedKey( + out, err := validator(tc.ek).getTrustedKey( context.Background(), vtpm.AttestationDocument{ Attestation: &attest.Attestation{ @@ -68,142 +62,12 @@ func TestGetTrustedKey(t *testing.T) { nil, ) - if tc.wantErr { - assert.Error(err) - } else { - assert.NoError(err) - } - - assert.Nil(out) - }) - } -} - -// TestValidateSNPReport has to setup the following to run ValidateSNPReport: -// - parse ARK certificate from constants.go. -// - parse cached ASK certificate. -// - parse cached SNP report. -// - parse cached AK hash. Hash and SNP report have to match. -// - parse cache VLEK cert. -func TestValidateSNPReport(t *testing.T) { - require := require.New(t) - certs, err := loadCerts(testdata.CertChain) - require.NoError(err) - ark := certs[1] - ask := certs[0] - - // reportTransformer unpacks the base64 encoded report, applies the given transformations and re-encodes it. - reportTransformer := func(reportHex string, transformations func(*spb.Report)) string { - rawReport, err := base64.StdEncoding.DecodeString(reportHex) - require.NoError(err) - report, err := abi.ReportToProto(rawReport) - require.NoError(err) - transformations(report) - reportBytes, err := abi.ReportToAbiBytes(report) - require.NoError(err) - return base64.StdEncoding.EncodeToString(reportBytes) - } - - testCases := map[string]struct { - ak string - report string - reportTransformer func(string, func(*spb.Report)) string - verifier reportVerifier - validator reportValidator - wantErr bool - }{ - "success": { - ak: testdata.AKDigest, - report: testdata.SNPReport, - verifier: &reportVerifierImpl{}, - validator: &reportValidatorImpl{}, - }, - "invalid report data": { - ak: testdata.AKDigest, - report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { - r.ReportData = make([]byte, 64) - }), - verifier: &stubReportVerifier{}, - validator: &reportValidatorImpl{}, - wantErr: true, - }, - "invalid report signature": { - ak: testdata.AKDigest, - report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { r.Signature[0]++ }), - verifier: &reportVerifierImpl{}, - validator: &reportValidatorImpl{}, - wantErr: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - hash, err := hex.DecodeString(tc.ak) - require.NoError(err) - - report, err := base64.StdEncoding.DecodeString(tc.report) - require.NoError(err) - - info := snp.InstanceInfo{AttestationReport: report, ReportSigner: testdata.VLEK} - infoMarshalled, err := json.Marshal(info) - require.NoError(err) - - v := gcpValidator{httpsGetter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), verifier: tc.verifier, validator: tc.validator} - err = v.validate(vtpm.AttestationDocument{InstanceInfo: infoMarshalled}, ask, ark, [64]byte(hash), config.DefaultForGCPSEVSNP(), logger.NewTest(t)) - if tc.wantErr { - assert.Error(err) - } else { - assert.NoError(err) - } + assert.NoError(err) + assert.Equal(tc.ek, out) }) } } -type stubHTTPSGetter struct { - urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs - err error -} - -func newStubHTTPSGetter(urlResponseMatcher *urlResponseMatcher, err error) *stubHTTPSGetter { - return &stubHTTPSGetter{ - urlResponseMatcher: urlResponseMatcher, - err: err, - } -} - -func (s *stubHTTPSGetter) Get(url string) ([]byte, error) { - if s.err != nil { - return nil, s.err - } - return s.urlResponseMatcher.match(url) -} - -type urlResponseMatcher struct { - certChainResponse []byte - wantCertChainRequest bool - vcekResponse []byte - wantVcekRequest bool -} - -func (m *urlResponseMatcher) match(url string) ([]byte, error) { - switch { - case url == "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": - if !m.wantCertChainRequest { - return nil, fmt.Errorf("unexpected cert_chain request") - } - return m.certChainResponse, nil - case regexp.MustCompile(`https:\/\/kdsintf.amd.com\/vcek\/v1\/Milan\/.*`).MatchString(url): - if !m.wantVcekRequest { - return nil, fmt.Errorf("unexpected VCEK request") - } - return m.vcekResponse, nil - default: - return nil, fmt.Errorf("unexpected URL: %s", url) - } -} - func TestSha512sum(t *testing.T) { testCases := map[string]struct { key string From 1657b127c4cf38432d48cb6379dc5336c430bb28 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:19:02 +0200 Subject: [PATCH 18/30] Update cli/internal/cmd/verify.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> --- cli/internal/cmd/verify.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 5b79f9346f..37b6726f6f 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -467,7 +467,8 @@ func updateInitMeasurements(config config.AttestationCfg, ownerID, clusterID str switch config.GetVariant() { case variant.AWSNitroTPM{}, variant.AWSSEVSNP{}, variant.AzureTrustedLaunch{}, variant.AzureSEVSNP{}, variant.AzureTDX{}, // AzureTDX also uses a vTPM for measurements - variant.GCPSEVES{}, variant.GCPSEVSNP{}, variant.QEMUVTPM{}: + variant.GCPSEVES{}, variant.GCPSEVSNP{}, + variant.QEMUVTPM{}: if err := updateMeasurementTPM(m, uint32(measurements.PCRIndexOwnerID), ownerID); err != nil { return err } From 75be502490a260149e6beb16fa0cf7ab05da188a Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:19:12 +0200 Subject: [PATCH 19/30] Update docs/docs/overview/clouds.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> --- docs/docs/overview/clouds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/overview/clouds.md b/docs/docs/overview/clouds.md index 4f6b60187d..a7b1361e86 100644 --- a/docs/docs/overview/clouds.md +++ b/docs/docs/overview/clouds.md @@ -5,7 +5,7 @@ What works on which cloud? Currently, Confidential VMs (CVMs) are available in v For Constellation, the ideal environment provides the following: 1. Ability to run arbitrary software and images inside CVMs -2. CVMs based on AMD SEV-SNP (available in EPYC CPUs since the Milan generation) or Intel TDX (available in Xeon CPUs since the Sapphire Rapid generation) +2. CVMs based on AMD SEV-SNP (available in EPYC CPUs since the Milan generation) or Intel TDX (available in Xeon CPUs since the Sapphire Rapids generation) 3. Ability for CVM guests to obtain raw hardware attestation statements 4. Reviewable, open-source firmware inside CVMs 5. Capability of the firmware to attest the integrity of the code it passes control to, e.g., with an embedded virtual TPM (vTPM) From bea33f349fad25357766e7ed6513f559eb1b3269 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:19:23 +0200 Subject: [PATCH 20/30] Update terraform-provider-constellation/internal/provider/attestation_data_source_test.go Co-authored-by: Adrian Stobbe --- .../internal/provider/attestation_data_source_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform-provider-constellation/internal/provider/attestation_data_source_test.go b/terraform-provider-constellation/internal/provider/attestation_data_source_test.go index ddb7ed7a85..3740e6b114 100644 --- a/terraform-provider-constellation/internal/provider/attestation_data_source_test.go +++ b/terraform-provider-constellation/internal/provider/attestation_data_source_test.go @@ -128,7 +128,7 @@ func TestAccAttestationSource(t *testing.T) { // } // `, // Check: resource.ComposeAggregateTestCheckFunc( - // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.variant", "gcp-sev-es"), + // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.variant", "gcp-sev-snp"), // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.bootloader_version", "0"), // since this is not supported on GCP, we expect 0 // resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.measurements.1.expected", "745f2fb4235e4647aa0ad5ace781cd929eb68c28870e7dd5d1a1535854325e56"), From 55d865887f7b855a787e976046caa28a2ee9b15e Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:15:25 +0200 Subject: [PATCH 21/30] linter fixes --- internal/attestation/gcp/snp/BUILD.bazel | 2 - .../attestation/gcp/snp/validator_test.go | 38 +------------------ 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/internal/attestation/gcp/snp/BUILD.bazel b/internal/attestation/gcp/snp/BUILD.bazel index 1193a7e036..3bd936a150 100644 --- a/internal/attestation/gcp/snp/BUILD.bazel +++ b/internal/attestation/gcp/snp/BUILD.bazel @@ -43,8 +43,6 @@ go_test( "//internal/attestation", "//internal/attestation/vtpm", "//internal/config", - "@com_github_google_go_sev_guest//proto/sevsnp", - "@com_github_google_go_sev_guest//verify", "@com_github_google_go_tpm_tools//proto/attest", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", diff --git a/internal/attestation/gcp/snp/validator_test.go b/internal/attestation/gcp/snp/validator_test.go index 6c624e55a7..70fb60b008 100644 --- a/internal/attestation/gcp/snp/validator_test.go +++ b/internal/attestation/gcp/snp/validator_test.go @@ -12,16 +12,12 @@ import ( "crypto" "crypto/x509" "encoding/hex" - "encoding/pem" - "errors" "fmt" "testing" "github.com/edgelesssys/constellation/v2/internal/attestation" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/constellation/v2/internal/config" - "github.com/google/go-sev-guest/proto/sevsnp" - "github.com/google/go-sev-guest/verify" "github.com/google/go-tpm-tools/proto/attest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -31,7 +27,7 @@ func TestGetTrustedKey(t *testing.T) { validator := func(ek []byte) *Validator { return &Validator{ reportValidator: stubGCPValidator{}, - gceKeyGetter: func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { + gceKeyGetter: func(_ context.Context, _ vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { return ek, nil }, } @@ -120,40 +116,8 @@ func loadKeyFromHex(key string) (crypto.PublicKey, error) { return x509.ParsePKIXPublicKey(decoded) } -// loadCachedCertChain loads a valid ARK and ASK from the testdata folder. -func loadCerts(pemData []byte) ([]*x509.Certificate, error) { - var certs []*x509.Certificate - - for len(pemData) > 0 { - var block *pem.Block - block, pemData = pem.Decode(pemData) - if block == nil { - break - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, err - } - - certs = append(certs, cert) - } - - if len(certs) == 0 { - return nil, errors.New("no valid certificates found") - } - - return certs, nil -} - type stubGCPValidator struct{} func (stubGCPValidator) validate(_ vtpm.AttestationDocument, _ *x509.Certificate, _ *x509.Certificate, _ [64]byte, _ *config.GCPSEVSNP, _ attestation.Logger) error { return nil } - -type stubReportVerifier struct{} - -func (stubReportVerifier) SnpAttestation(_ *sevsnp.Attestation, _ *verify.Options) error { - return nil -} From b4811b8b761a2917efab0dad8ba4e7b25d02efa0 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:26:18 +0200 Subject: [PATCH 22/30] terraform_provider: correctly pass down CC technology --- .github/workflows/e2e-test-provider-example.yml | 10 ++++++++++ .../examples/full/gcp/main.tf | 2 ++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/e2e-test-provider-example.yml b/.github/workflows/e2e-test-provider-example.yml index a011953e3e..f2b77fd099 100644 --- a/.github/workflows/e2e-test-provider-example.yml +++ b/.github/workflows/e2e-test-provider-example.yml @@ -266,11 +266,21 @@ jobs: run: | region=$(echo ${{ inputs.regionZone || 'europe-west3-b' }} | rev | cut -c 3- | rev) + case "${{ inputs.attestationVariant }}" in + "gcp-sev-snp") + cc_tech="SEV_SNP" + ;; + *) + cc_tech="SEV" + ;; + esac + cat >> _override.tf < Date: Fri, 5 Apr 2024 15:31:33 +0200 Subject: [PATCH 23/30] config: mark attestationconfigapi as unimplemented --- .../actions/constellation_create/action.yml | 1 - internal/config/gcp.go | 58 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/.github/actions/constellation_create/action.yml b/.github/actions/constellation_create/action.yml index 7e149fc18b..d845059164 100644 --- a/.github/actions/constellation_create/action.yml +++ b/.github/actions/constellation_create/action.yml @@ -164,7 +164,6 @@ runs: shell: bash run: | echo "Creating cluster using config:" - cat constellation-conf.yaml sudo sh -c 'echo "127.0.0.1 license.confidential.cloud" >> /etc/hosts' || true - name: Constellation create (CLI) diff --git a/internal/config/gcp.go b/internal/config/gcp.go index 299af595f2..c90342c737 100644 --- a/internal/config/gcp.go +++ b/internal/config/gcp.go @@ -72,35 +72,39 @@ func (c *GCPSEVSNP) getToMarshallLatestWithResolvedVersions() AttestationCfg { } // 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) FetchAndSetLatestVersionNumbers(_ context.Context, _ attestationconfigapi.Fetcher) error { + panic("not implemented") + + // TODO(msanft): Implement with https://dev.azure.com/Edgeless/Edgeless/_workitems/edit/4024 + + // // 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 - } -} +// 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 { From 5f14a82de473e739c8cccf3c94610a71994cfde1 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:40:11 +0200 Subject: [PATCH 24/30] gcp: fix comments and typos --- .github/actions/constellation_create/action.yml | 1 + internal/attestation/gcp/es/es.go | 2 +- internal/attestation/gcp/gcp.go | 2 +- internal/attestation/gcp/snp/issuer.go | 4 ++-- internal/attestation/gcp/snp/snp.go | 8 +++----- internal/attestation/gcp/snp/validator.go | 2 +- internal/attestation/vtpm/attestation.go | 2 +- internal/config/gcp.go | 1 + 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/actions/constellation_create/action.yml b/.github/actions/constellation_create/action.yml index d845059164..7e149fc18b 100644 --- a/.github/actions/constellation_create/action.yml +++ b/.github/actions/constellation_create/action.yml @@ -164,6 +164,7 @@ runs: shell: bash run: | echo "Creating cluster using config:" + cat constellation-conf.yaml sudo sh -c 'echo "127.0.0.1 license.confidential.cloud" >> /etc/hosts' || true - name: Constellation create (CLI) diff --git a/internal/attestation/gcp/es/es.go b/internal/attestation/gcp/es/es.go index b5951b9e51..7a6dfe4464 100644 --- a/internal/attestation/gcp/es/es.go +++ b/internal/attestation/gcp/es/es.go @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only */ /* -# GCP SEV-ES Attestation +# GCP SEV-ES attestation Google offers [confidential VMs], utilizing AMD SEV-ES to provide memory encryption. diff --git a/internal/attestation/gcp/gcp.go b/internal/attestation/gcp/gcp.go index b9a9a4ac58..113222dda2 100644 --- a/internal/attestation/gcp/gcp.go +++ b/internal/attestation/gcp/gcp.go @@ -5,6 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only */ /* -# Google Cloud Platform Attestation +# Google Cloud Platform attestation */ package gcp diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go index 951253e778..e54d870d7d 100644 --- a/internal/attestation/gcp/snp/issuer.go +++ b/internal/attestation/gcp/snp/issuer.go @@ -85,7 +85,7 @@ func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byt vcek, err := pemEncodedVCEK(certs) if err != nil { - return nil, fmt.Errorf("parsing vlek: %w", err) + return nil, fmt.Errorf("parsing vcek: %w", err) } gceInstanceInfo, err := gceInstanceInfo() @@ -145,7 +145,7 @@ func pemEncodedVCEK(certs []byte) ([]byte, error) { return nil, fmt.Errorf("getting VCEK certificate: %w", err) } - // An optional check for certificate well-formedness. vlekRaw == cert.Raw. + // An optional check for certificate well-formedness. vcekRaw == cert.Raw. cert, err := x509.ParseCertificate(vcekRaw) if err != nil { return nil, fmt.Errorf("parsing certificate: %w", err) diff --git a/internal/attestation/gcp/snp/snp.go b/internal/attestation/gcp/snp/snp.go index f81a6df2cf..ede60f2053 100644 --- a/internal/attestation/gcp/snp/snp.go +++ b/internal/attestation/gcp/snp/snp.go @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only */ /* -# GCP SEV-SNP Attestation +# GCP SEV-SNP attestation Google offers [confidential VMs], utilizing AMD SEV-SNP to provide memory encryption. @@ -36,9 +36,7 @@ public key provided by Google's API corresponding to the project ID, zone, insta Without a certificate signing the authenticity of any endorsement keys we have no way of establishing a chain of trust. Instead, we have to rely on Google's API to provide us with the public key of the vTPM's endorsement key. -[GCP Confidential VMs]: https://cloud.google.com/compute/confidential-vm/docs/about-cvm -[GCP Virtual Trusted Platform Module (vTPM)]: https://cloud.google.com/security/shielded-cloud/shielded-vm#vtpm -[GCP Monitoring docs]: https://cloud.google.com/compute/confidential-vm/docs/monitoring -[AMD SEV-SNP whitepaper]: https://www.amd.com/system/files/TechDocs/SEV-SNP-strengthening-vm-isolation-with-integrity-protection-and-more.pdf#page=7 +[confidential VMs]: https://cloud.google.com/compute/confidential-vm/docs/about-cvm +[virtual Trusted Platform Module (vTPM)]: https://cloud.google.com/security/shielded-cloud/shielded-vm#vtpm */ package snp diff --git a/internal/attestation/gcp/snp/validator.go b/internal/attestation/gcp/snp/validator.go index 32466a54fb..dba4ab0fae 100644 --- a/internal/attestation/gcp/snp/validator.go +++ b/internal/attestation/gcp/snp/validator.go @@ -49,7 +49,7 @@ type Validator struct { func NewValidator(cfg *config.GCPSEVSNP, log attestation.Logger) (*Validator, error) { getGCEKey, err := gcp.TrustedKeyGetter(variant.GCPSEVSNP{}, gcp.NewRESTClient) if err != nil { - return nil, fmt.Errorf("create trusted key getter: %v", err) + return nil, fmt.Errorf("creating trusted key getter: %w", err) } v := &Validator{ diff --git a/internal/attestation/vtpm/attestation.go b/internal/attestation/vtpm/attestation.go index f0e233f5d3..364ab11632 100644 --- a/internal/attestation/vtpm/attestation.go +++ b/internal/attestation/vtpm/attestation.go @@ -125,7 +125,6 @@ func (i *Issuer) Issue(ctx context.Context, userData []byte, nonce []byte) (res } defer aK.Close() - // Create an attestation using the loaded key extraData := attestation.MakeExtraData(userData, nonce) // Fetch instance info of the VM @@ -136,6 +135,7 @@ func (i *Issuer) Issue(ctx context.Context, userData []byte, nonce []byte) (res tpmNonce := makeTpmNonce(instanceInfo, extraData) + // Create an attestation using the loaded key tpmAttestation, err := aK.Attest(tpmClient.AttestOpts{Nonce: tpmNonce[:]}) if err != nil { return nil, fmt.Errorf("creating attestation: %w", err) diff --git a/internal/config/gcp.go b/internal/config/gcp.go index c90342c737..b4cc590c66 100644 --- a/internal/config/gcp.go +++ b/internal/config/gcp.go @@ -2,6 +2,7 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ + package config import ( From 4e3582ec892740c807234e237096d7ae717954c6 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:04:35 +0200 Subject: [PATCH 25/30] snp: use nonce and PK hash in SNP report --- internal/attestation/gcp/snp/issuer.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go index e54d870d7d..e4b122af18 100644 --- a/internal/attestation/gcp/snp/issuer.go +++ b/internal/attestation/gcp/snp/issuer.go @@ -8,7 +8,6 @@ package snp import ( "context" - "crypto/sha512" "crypto/x509" "encoding/json" "encoding/pem" @@ -59,18 +58,12 @@ func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { // getInstanceInfo generates an extended SNP report, i.e. the report and any loaded certificates. // Report generation is triggered by sending ioctl syscalls to the SNP guest device, the AMD PSP generates the report. // The returned bytes will be written into the attestation document. -func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) { - tpmAk, err := client.GceAttestationKeyRSA(tpm) - if err != nil { - return nil, fmt.Errorf("creating RSA Endorsement key: %w", err) +func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, extraData []byte) ([]byte, error) { + if len(extraData) > 64 { + return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) } - - encoded, err := x509.MarshalPKIXPublicKey(tpmAk.PublicKey()) - if err != nil { - return nil, fmt.Errorf("marshalling public key: %w", err) - } - - akDigest := sha512.Sum512(encoded) + truncatedExtraData := make([]byte, 64) + copy(truncatedExtraData, extraData) device, err := sevclient.OpenDevice() if err != nil { @@ -78,7 +71,7 @@ func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byt } defer device.Close() - report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, akDigest, 0) + report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, [64]byte(truncatedExtraData), 0) if err != nil { return nil, fmt.Errorf("getting extended report: %w", err) } From 500196db6ab83062ea4655fefe1464b10e238dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= <66256922+daniel-weisse@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:59:30 +0200 Subject: [PATCH 26/30] snp: ensure we never use ARK supplied by Issuer (#3025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make sure SNP ARK is always loaded from config, or fetched from AMD KDS * GCP: Set validator `reportData` correctly --------- Signed-off-by: Daniel Weiße Co-authored-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --- internal/attestation/aws/snp/validator.go | 4 +- internal/attestation/azure/snp/validator.go | 46 ++++--- internal/attestation/gcp/snp/BUILD.bazel | 21 --- internal/attestation/gcp/snp/issuer.go | 37 ++++-- internal/attestation/gcp/snp/validator.go | 51 ++------ .../attestation/gcp/snp/validator_test.go | 123 ------------------ internal/attestation/snp/BUILD.bazel | 1 - internal/attestation/snp/snp.go | 19 ++- internal/attestation/snp/snp_test.go | 43 +++--- 9 files changed, 100 insertions(+), 245 deletions(-) delete mode 100644 internal/attestation/gcp/snp/validator_test.go diff --git a/internal/attestation/aws/snp/validator.go b/internal/attestation/aws/snp/validator.go index 22d8b814b1..873851c73b 100644 --- a/internal/attestation/aws/snp/validator.go +++ b/internal/attestation/aws/snp/validator.go @@ -191,11 +191,11 @@ func (a *awsValidator) validate(attestation vtpm.AttestationDocument, ask *x509. func getVerifyOpts(att *sevsnp.Attestation) (*verify.Options, error) { ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) if err != nil { - return &verify.Options{}, fmt.Errorf("parsing VLEK certificate: %w", err) + return nil, fmt.Errorf("parsing ASK certificate: %w", err) } ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) if err != nil { - return &verify.Options{}, fmt.Errorf("parsing VLEK certificate: %w", err) + return nil, fmt.Errorf("parsing ARK certificate: %w", err) } verifyOpts := &verify.Options{ diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index a4b58e4d40..d3563d06ac 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -116,25 +116,11 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo return nil, fmt.Errorf("parsing attestation report: %w", err) } - // ASK, as cached in joinservice or reported from THIM / KDS. - ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) + verifyOpts, err := getVerifyOpts(att) if err != nil { - return nil, fmt.Errorf("parsing ASK certificate: %w", err) + return nil, fmt.Errorf("getting verify options: %w", err) } - verifyOpts := &verify.Options{ - TrustedRoots: map[string][]*trust.AMDRootCerts{ - "Milan": { - { - Product: "Milan", - ProductCerts: &trust.ProductCerts{ - Ask: ask, - Ark: trustedArk, - }, - }, - }, - }, - } if err := v.attestationVerifier.SNPAttestation(att, verifyOpts); err != nil { return nil, fmt.Errorf("verifying SNP attestation: %w", err) } @@ -252,3 +238,31 @@ type maaValidator interface { type hclAkValidator interface { Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error } + +func getVerifyOpts(att *spb.Attestation) (*verify.Options, error) { + // ASK, as cached in joinservice or reported from THIM / KDS. + ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) + if err != nil { + return nil, fmt.Errorf("parsing ASK certificate: %w", err) + } + ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) + if err != nil { + return nil, fmt.Errorf("parsing ARK certificate: %w", err) + } + + verifyOpts := &verify.Options{ + TrustedRoots: map[string][]*trust.AMDRootCerts{ + "Milan": { + { + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + Ask: ask, + Ark: ark, + }, + }, + }, + }, + } + + return verifyOpts, nil +} diff --git a/internal/attestation/gcp/snp/BUILD.bazel b/internal/attestation/gcp/snp/BUILD.bazel index 3bd936a150..cef1ff9c81 100644 --- a/internal/attestation/gcp/snp/BUILD.bazel +++ b/internal/attestation/gcp/snp/BUILD.bazel @@ -1,5 +1,4 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//bazel/go:go_test.bzl", "go_test") go_library( name = "snp", @@ -24,27 +23,7 @@ go_library( "@com_github_google_go_sev_guest//validate", "@com_github_google_go_sev_guest//verify", "@com_github_google_go_sev_guest//verify/trust", - "@com_github_google_go_tpm//legacy/tpm2", "@com_github_google_go_tpm_tools//client", "@com_github_google_go_tpm_tools//proto/attest", ], ) - -go_test( - name = "snp_test", - srcs = ["validator_test.go"], - embed = [":snp"], - # keep - gotags = select({ - "//bazel/settings:tpm_simulator_enabled": [], - "//conditions:default": ["disable_tpm_simulator"], - }), - deps = [ - "//internal/attestation", - "//internal/attestation/vtpm", - "//internal/config", - "@com_github_google_go_tpm_tools//proto/attest", - "@com_github_stretchr_testify//assert", - "@com_github_stretchr_testify//require", - ], -) diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go index e4b122af18..806baa2619 100644 --- a/internal/attestation/gcp/snp/issuer.go +++ b/internal/attestation/gcp/snp/issuer.go @@ -58,7 +58,7 @@ func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { // getInstanceInfo generates an extended SNP report, i.e. the report and any loaded certificates. // Report generation is triggered by sending ioctl syscalls to the SNP guest device, the AMD PSP generates the report. // The returned bytes will be written into the attestation document. -func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, extraData []byte) ([]byte, error) { +func getInstanceInfo(_ context.Context, _ io.ReadWriteCloser, extraData []byte) ([]byte, error) { if len(extraData) > 64 { return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) } @@ -76,7 +76,7 @@ func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, extraData []byte return nil, fmt.Errorf("getting extended report: %w", err) } - vcek, err := pemEncodedVCEK(certs) + vcek, certChain, err := parseSNPCertTable(certs) if err != nil { return nil, fmt.Errorf("parsing vcek: %w", err) } @@ -89,6 +89,7 @@ func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, extraData []byte raw, err := json.Marshal(snp.InstanceInfo{ AttestationReport: report, ReportSigner: vcek, + CertChain: certChain, GCP: gceInstanceInfo, }) if err != nil { @@ -124,30 +125,44 @@ func gceInstanceInfo() (*attest.GCEInstanceInfo, error) { }, nil } -// pemEncodedVCEK takes a marshalled SNP certificate table and returns the PEM-encoded VCEK certificate. +// parseSNPCertTable takes a marshalled SNP certificate table and returns the PEM-encoded VCEK certificate and, +// if present, the ASK of the SNP certificate chain. // AMD documentation on certificate tables can be found in section 4.1.8.1, revision 2.03 "SEV-ES Guest-Hypervisor Communication Block Standardization". // https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/56421.pdf -func pemEncodedVCEK(certs []byte) ([]byte, error) { +func parseSNPCertTable(certs []byte) (vcekPEM []byte, certChain []byte, err error) { certTable := abi.CertTable{} if err := certTable.Unmarshal(certs); err != nil { - return nil, fmt.Errorf("unmarshalling SNP certificate table: %w", err) + return nil, nil, fmt.Errorf("unmarshalling SNP certificate table: %w", err) } vcekRaw, err := certTable.GetByGUIDString(abi.VcekGUID) if err != nil { - return nil, fmt.Errorf("getting VCEK certificate: %w", err) + return nil, nil, fmt.Errorf("getting VCEK certificate: %w", err) } // An optional check for certificate well-formedness. vcekRaw == cert.Raw. - cert, err := x509.ParseCertificate(vcekRaw) + vcek, err := x509.ParseCertificate(vcekRaw) if err != nil { - return nil, fmt.Errorf("parsing certificate: %w", err) + return nil, nil, fmt.Errorf("parsing certificate: %w", err) } - certPEM := pem.EncodeToMemory(&pem.Block{ + vcekPEM = pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", - Bytes: cert.Raw, + Bytes: vcek.Raw, }) - return certPEM, nil + var askPEM []byte + if askRaw, err := certTable.GetByGUIDString(abi.AskGUID); err == nil { + ask, err := x509.ParseCertificate(askRaw) + if err != nil { + return nil, nil, fmt.Errorf("parsing ASK certificate: %w", err) + } + + askPEM = pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: ask.Raw, + }) + } + + return vcekPEM, askPEM, nil } diff --git a/internal/attestation/gcp/snp/validator.go b/internal/attestation/gcp/snp/validator.go index dba4ab0fae..cd72366d7e 100644 --- a/internal/attestation/gcp/snp/validator.go +++ b/internal/attestation/gcp/snp/validator.go @@ -9,7 +9,6 @@ package snp import ( "context" "crypto" - "crypto/sha512" "crypto/x509" "encoding/json" "fmt" @@ -27,7 +26,6 @@ import ( "github.com/google/go-sev-guest/verify" "github.com/google/go-sev-guest/verify/trust" "github.com/google/go-tpm-tools/proto/attest" - "github.com/google/go-tpm/legacy/tpm2" ) // Validator for GCP SEV-SNP / TPM attestation. @@ -62,53 +60,30 @@ func NewValidator(cfg *config.GCPSEVSNP, log attestation.Logger) (*Validator, er v.Validator = vtpm.NewValidator( cfg.Measurements, v.getTrustedKey, - v.validateCVM, + func(_ vtpm.AttestationDocument, _ *attest.MachineState) error { return nil }, log, ) return v, nil } // getTrustedKey returns TPM endorsement key provided through the GCE metadata API. -func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { +func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, extraData []byte) (crypto.PublicKey, error) { ekPub, err := v.gceKeyGetter(ctx, attDoc, nil) if err != nil { return nil, fmt.Errorf("getting TPM endorsement key: %w", err) } - return ekPub, nil -} - -// validateCVM validates the SEV-SNP attestation document. -func (v *Validator) validateCVM(attDoc vtpm.AttestationDocument, _ *attest.MachineState) error { - pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub) - if err != nil { - return fmt.Errorf("decoding public area: %w", err) - } - - pubKey, err := pubArea.Key() - if err != nil { - return fmt.Errorf("getting public key: %w", err) - } - - akDigest, err := sha512sum(pubKey) - if err != nil { - return fmt.Errorf("calculating hash of attestation key: %w", err) - } - - if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), akDigest, v.cfg, v.log); err != nil { - return fmt.Errorf("validating SNP report: %w", err) + if len(extraData) > 64 { + return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) } - return nil -} + extraData64 := make([]byte, 64) + copy(extraData64, extraData) -// sha512sum PEM-encodes a public key and calculates the SHA512 hash of the encoded key. -func sha512sum(key crypto.PublicKey) ([64]byte, error) { - pub, err := x509.MarshalPKIXPublicKey(key) - if err != nil { - return [64]byte{}, fmt.Errorf("marshalling public key: %w", err) + if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), [64]byte(extraData64), v.cfg, v.log); err != nil { + return nil, fmt.Errorf("validating SNP report: %w", err) } - return sha512.Sum512(pub), nil + return ekPub, nil } // snpReportValidator validates a given SNP report. @@ -146,7 +121,7 @@ func (r *reportVerifierImpl) SnpAttestation(att *sevsnp.Attestation, opts *verif // validate the report by checking if it has a valid VCEK signature. // The certificate chain ARK -> ASK -> VCEK is also validated. // Checks that the report's userData matches the connection's userData. -func (a *gcpValidator) validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, akDigest [64]byte, config *config.GCPSEVSNP, log attestation.Logger) error { +func (a *gcpValidator) validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, reportData [64]byte, config *config.GCPSEVSNP, log attestation.Logger) error { var info snp.InstanceInfo if err := json.Unmarshal(attestation.InstanceInfo, &info); err != nil { return fmt.Errorf("unmarshalling instance info: %w", err) @@ -170,7 +145,7 @@ func (a *gcpValidator) validate(attestation vtpm.AttestationDocument, ask *x509. validateOpts := &validate.Options{ // Check that the attestation key's digest is included in the report. - ReportData: akDigest[:], + ReportData: reportData[:], GuestPolicy: abi.SnpPolicy{ Debug: false, // Debug means the VM can be decrypted by the host for debugging purposes and thus is not allowed. SMT: true, // Allow Simultaneous Multi-Threading (SMT). Normally, we would want to disable SMT @@ -205,11 +180,11 @@ func (a *gcpValidator) validate(attestation vtpm.AttestationDocument, ask *x509. func getVerifyOpts(att *sevsnp.Attestation) (*verify.Options, error) { ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) if err != nil { - return &verify.Options{}, fmt.Errorf("parsing ASK certificate: %w", err) + return nil, fmt.Errorf("parsing ASK certificate: %w", err) } ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) if err != nil { - return &verify.Options{}, fmt.Errorf("parsing ARK certificate: %w", err) + return nil, fmt.Errorf("parsing ARK certificate: %w", err) } verifyOpts := &verify.Options{ diff --git a/internal/attestation/gcp/snp/validator_test.go b/internal/attestation/gcp/snp/validator_test.go deleted file mode 100644 index 70fb60b008..0000000000 --- a/internal/attestation/gcp/snp/validator_test.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package snp - -import ( - "bytes" - "context" - "crypto" - "crypto/x509" - "encoding/hex" - "fmt" - "testing" - - "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" - "github.com/edgelesssys/constellation/v2/internal/config" - "github.com/google/go-tpm-tools/proto/attest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetTrustedKey(t *testing.T) { - validator := func(ek []byte) *Validator { - return &Validator{ - reportValidator: stubGCPValidator{}, - gceKeyGetter: func(_ context.Context, _ vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { - return ek, nil - }, - } - } - testCases := map[string]struct { - akPub []byte - ek []byte - info []byte - }{ - "success": { - akPub: []byte("akPub"), - ek: []byte("ek"), - info: []byte("info"), - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - out, err := validator(tc.ek).getTrustedKey( - context.Background(), - vtpm.AttestationDocument{ - Attestation: &attest.Attestation{ - AkPub: tc.akPub, - }, - InstanceInfo: tc.info, - }, - nil, - ) - - assert.NoError(err) - assert.Equal(tc.ek, out) - }) - } -} - -func TestSha512sum(t *testing.T) { - testCases := map[string]struct { - key string - hash string - match bool - }{ - "success": { - // Generated using: rsa.GenerateKey(rand.Reader, 1024). - key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", - hash: "2d6fe5ec59d7240b8a4c27c2ff27ba1071105fa50d45543768fcbabf9ee3cb8f8fa0afa51e08e053af30f6d11066ebfd47e75bda5ccc085c115d7e1896f3c62f", - match: true, - }, - "mismatching hash": { - key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", - hash: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - match: false, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - newKey, err := loadKeyFromHex(tc.key) - require.NoError(err) - - // Function under test: - hash, err := sha512sum(newKey) - assert.NoError(err) - - expected, err := hex.DecodeString(tc.hash) - require.NoError(err) - - if tc.match { - assert.True(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected hash %x, got %x", expected, hash)) - } else { - assert.False(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected mismatching hashes, got %x", hash)) - } - }) - } -} - -func loadKeyFromHex(key string) (crypto.PublicKey, error) { - decoded, err := hex.DecodeString(key) - if err != nil { - return nil, err - } - - return x509.ParsePKIXPublicKey(decoded) -} - -type stubGCPValidator struct{} - -func (stubGCPValidator) validate(_ vtpm.AttestationDocument, _ *x509.Certificate, _ *x509.Certificate, _ [64]byte, _ *config.GCPSEVSNP, _ attestation.Logger) error { - return nil -} diff --git a/internal/attestation/snp/BUILD.bazel b/internal/attestation/snp/BUILD.bazel index 1a1bd25044..f62518f254 100644 --- a/internal/attestation/snp/BUILD.bazel +++ b/internal/attestation/snp/BUILD.bazel @@ -8,7 +8,6 @@ go_library( visibility = ["//:__subpackages__"], deps = [ "//internal/attestation", - "//internal/constants", "@com_github_google_go_sev_guest//abi", "@com_github_google_go_sev_guest//kds", "@com_github_google_go_sev_guest//proto/sevsnp", diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index e9db5f5a54..4079aa78ee 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -15,7 +15,6 @@ import ( "fmt" "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/kds" spb "github.com/google/go-sev-guest/proto/sevsnp" @@ -97,7 +96,7 @@ func (a *InstanceInfo) addReportSigner(att *spb.Attestation, report *spb.Report, // AttestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo. // Certificates are retrieved in the following precedence: -// 1. ASK or ARK from issuer. On Azure: THIM. One AWS: not prefilled. +// 1. ASK from issuer. On Azure: THIM. One AWS: not prefilled. (Go to option 2) On GCP: prefilled. // 2. ASK or ARK from fallbackCerts. // 3. ASK or ARK from AMD KDS. func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter, @@ -122,19 +121,16 @@ func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter, return nil, fmt.Errorf("adding report signer: %w", err) } - // If the certificate chain from THIM is present, parse it and format it. - ask, ark, err := a.ParseCertChain() + // If a certificate chain was pre-fetched by the Issuer, parse it and format it. + // Make sure to only use the ask, since using an ark from the Issuer would invalidate security guarantees. + ask, _, err := a.ParseCertChain() if err != nil { logger.Warn(fmt.Sprintf("Error parsing certificate chain: %v", err)) } if ask != nil { - logger.Info("Using ASK certificate from Azure THIM") + logger.Info("Using ASK certificate from pre-fetched certificate chain") att.CertificateChain.AskCert = ask.Raw } - if ark != nil { - logger.Info("Using ARK certificate from Azure THIM") - att.CertificateChain.ArkCert = ark.Raw - } // If a cached ASK or an ARK from the Constellation config is present, use it. if att.CertificateChain.AskCert == nil && fallbackCerts.ask != nil { @@ -142,10 +138,11 @@ func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter, att.CertificateChain.AskCert = fallbackCerts.ask.Raw } if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil { - logger.Info(fmt.Sprintf("Using ARK certificate from %s", constants.ConfigFilename)) + logger.Info("Using cached ARK certificate") att.CertificateChain.ArkCert = fallbackCerts.ark.Raw } - // Otherwise, retrieve it from AMD KDS. + + // Otherwise, retrieve missing certificates from AMD KDS. if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil { logger.Info(fmt.Sprintf( "Certificate chain not fully present (ARK present: %t, ASK present: %t), falling back to retrieving it from AMD KDS", diff --git a/internal/attestation/snp/snp_test.go b/internal/attestation/snp/snp_test.go index 0179ac05b4..2eaf3e52a0 100644 --- a/internal/attestation/snp/snp_test.go +++ b/internal/attestation/snp/snp_test.go @@ -149,12 +149,24 @@ func TestAttestationWithCerts(t *testing.T) { wantErr bool }{ "success": { + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + reportSigner: testdata.AzureThimVCEK, + certChain: testdata.CertChain, + fallbackCerts: CertificateChain{ark: testdataArk}, + expectedArk: testdataArk, + expectedAsk: testdataAsk, + getter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), + }, + "ark only in pre-fetched cert-chain": { report: defaultReport, idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", reportSigner: testdata.AzureThimVCEK, certChain: testdata.CertChain, expectedArk: testdataArk, expectedAsk: testdataAsk, + getter: newStubHTTPSGetter(nil, assert.AnError), + wantErr: true, }, "vlek success": { report: vlekReport, @@ -173,9 +185,10 @@ func TestAttestationWithCerts(t *testing.T) { ), }, "retrieve vcek": { - report: defaultReport, - idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", - certChain: testdata.CertChain, + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + certChain: testdata.CertChain, + fallbackCerts: CertificateChain{ark: testdataArk}, getter: newStubHTTPSGetter( &urlResponseMatcher{ vcekResponse: testdata.AmdKdsVCEK, @@ -205,25 +218,9 @@ func TestAttestationWithCerts(t *testing.T) { idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", reportSigner: testdata.AzureThimVCEK, fallbackCerts: NewCertificateChain(exampleCert, exampleCert), - getter: newStubHTTPSGetter( - &urlResponseMatcher{}, - nil, - ), - expectedArk: exampleCert, - expectedAsk: exampleCert, - }, - "use certchain with fallback certs": { - report: defaultReport, - idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", - certChain: testdata.CertChain, - reportSigner: testdata.AzureThimVCEK, - fallbackCerts: NewCertificateChain(&x509.Certificate{}, &x509.Certificate{}), - getter: newStubHTTPSGetter( - &urlResponseMatcher{}, - nil, - ), - expectedArk: testdataArk, - expectedAsk: testdataAsk, + getter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), + expectedArk: exampleCert, + expectedAsk: exampleCert, }, "retrieve vcek and certchain": { report: defaultReport, @@ -242,10 +239,12 @@ func TestAttestationWithCerts(t *testing.T) { }, "report too short": { report: defaultReport[:len(defaultReport)-100], + getter: newStubHTTPSGetter(nil, assert.AnError), wantErr: true, }, "corrupted report": { report: defaultReport[10 : len(defaultReport)-10], + getter: newStubHTTPSGetter(nil, assert.AnError), wantErr: true, }, "certificate fetch error": { From 282216d0c14e835135803f2986d8408b3265a872 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:08:11 +0200 Subject: [PATCH 27/30] attestationconfigapi: add GCP to uploading --- .github/actions/e2e_verify/action.yml | 2 +- .../workflows/e2e-attestationconfigapi.yml | 2 +- .github/workflows/e2e-test-weekly.yml | 5 ++ cli/internal/cmd/verify.go | 7 ++- .../attestationconfigapi.go | 2 +- .../api/attestationconfigapi/cli/BUILD.bazel | 4 +- internal/api/attestationconfigapi/cli/aws.go | 24 -------- .../api/attestationconfigapi/cli/azure.go | 61 ------------------- .../api/attestationconfigapi/cli/delete.go | 59 ++++++++++++++++-- .../attestationconfigapi/cli/e2e/test.sh.in | 3 + .../api/attestationconfigapi/cli/upload.go | 12 ++-- internal/api/attestationconfigapi/client.go | 6 +- internal/api/attestationconfigapi/reporter.go | 6 +- internal/api/attestationconfigapi/snp.go | 10 +-- internal/attestation/gcp/snp/issuer.go | 6 +- internal/config/config.go | 6 ++ internal/config/gcp.go | 58 ++++++++---------- 17 files changed, 126 insertions(+), 147 deletions(-) delete mode 100644 internal/api/attestationconfigapi/cli/aws.go delete mode 100644 internal/api/attestationconfigapi/cli/azure.go diff --git a/.github/actions/e2e_verify/action.yml b/.github/actions/e2e_verify/action.yml index aca4fdceb4..c52d02f43b 100644 --- a/.github/actions/e2e_verify/action.yml +++ b/.github/actions/e2e_verify/action.yml @@ -84,7 +84,7 @@ runs: aws-region: eu-central-1 - name: Upload extracted TCBs - if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'aws-sev-snp') + if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'aws-sev-snp' || inputs.attestationVariant == 'gcp-sev-snp') shell: bash env: COSIGN_PASSWORD: ${{ inputs.cosignPassword }} diff --git a/.github/workflows/e2e-attestationconfigapi.yml b/.github/workflows/e2e-attestationconfigapi.yml index a3605dafce..e02c1d4db1 100644 --- a/.github/workflows/e2e-attestationconfigapi.yml +++ b/.github/workflows/e2e-attestationconfigapi.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false max-parallel: 1 matrix: - csp: ["azure", "aws"] + csp: ["azure", "aws", "gcp"] runs-on: ubuntu-22.04 permissions: id-token: write diff --git a/.github/workflows/e2e-test-weekly.yml b/.github/workflows/e2e-test-weekly.yml index d0a32c4ae5..5075cc7faf 100644 --- a/.github/workflows/e2e-test-weekly.yml +++ b/.github/workflows/e2e-test-weekly.yml @@ -281,6 +281,11 @@ jobs: attestationVariant: "gcp-sev-es" kubernetes-version: "v1.28" clusterCreation: "cli" + - test: "verify" + refStream: "ref/release/stream/stable/?" + attestationVariant: "gcp-sev-snp" + kubernetes-version: "v1.28" + clusterCreation: "cli" - test: "verify" refStream: "ref/release/stream/stable/?" attestationVariant: "azure-sev-snp" diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 37b6726f6f..5a84c3df0d 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -107,8 +107,11 @@ func runVerify(cmd *cobra.Command, _ []string) error { log: log, } formatterFactory := func(output string, attestation variant.Variant, log debugLog) (attestationDocFormatter, error) { - if output == "json" && (!attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{})) { - return nil, errors.New("json output is only supported for Azure SEV-SNP and AWS SEV-SNP") + if output == "json" && + (!attestation.Equal(variant.AzureSEVSNP{}) && + !attestation.Equal(variant.AWSSEVSNP{}) && + !attestation.Equal(variant.GCPSEVSNP{})) { + return nil, errors.New("json output is only supported for SEV-SNP") } switch output { case "json": diff --git a/internal/api/attestationconfigapi/attestationconfigapi.go b/internal/api/attestationconfigapi/attestationconfigapi.go index e170da869b..a0d84d7863 100644 --- a/internal/api/attestationconfigapi/attestationconfigapi.go +++ b/internal/api/attestationconfigapi/attestationconfigapi.go @@ -15,7 +15,7 @@ information contained in the objects. Especially the paths used for the API are in these helper methods. Regarding the decision to implement new types over using the existing types from internal/config: -AttesationCfg objects for AttestationCfg API need to hold some version information (for sorting, recognizing latest). +AttestationCfg objects for AttestationCfg API need to hold some version information (for sorting, recognizing latest). Thus, existing config types (AWSNitroTPM, AzureSEVSNP, ...) can not be extended to implement apiObject interface. Instead, we need a separate type that wraps _all_ attestation types. In the codebase this is done using the AttestationCfg interface. The new type AttestationCfgGet needs to be located inside internal/config in order to implement UnmarshalJSON. diff --git a/internal/api/attestationconfigapi/cli/BUILD.bazel b/internal/api/attestationconfigapi/cli/BUILD.bazel index 9825418299..1d2fe3ae61 100644 --- a/internal/api/attestationconfigapi/cli/BUILD.bazel +++ b/internal/api/attestationconfigapi/cli/BUILD.bazel @@ -10,8 +10,6 @@ go_binary( go_library( name = "cli_lib", srcs = [ - "aws.go", - "azure.go", "delete.go", "main.go", "upload.go", @@ -28,7 +26,7 @@ go_library( "//internal/logger", "//internal/staticupload", "//internal/verify", - "@com_github_aws_aws_sdk_go//aws", + "@com_github_aws_aws_sdk_go_v2//aws", "@com_github_aws_aws_sdk_go_v2_service_s3//:s3", "@com_github_aws_aws_sdk_go_v2_service_s3//types", "@com_github_spf13_afero//:afero", diff --git a/internal/api/attestationconfigapi/cli/aws.go b/internal/api/attestationconfigapi/cli/aws.go deleted file mode 100644 index 578caadc4f..0000000000 --- a/internal/api/attestationconfigapi/cli/aws.go +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package main - -import ( - "context" - "fmt" - - "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" - "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" -) - -func deleteAWS(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { - if cfg.provider != cloudprovider.AWS || cfg.kind != snpReport { - return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) - } - - return client.DeleteSEVSNPVersion(ctx, variant.AWSSEVSNP{}, cfg.version) -} diff --git a/internal/api/attestationconfigapi/cli/azure.go b/internal/api/attestationconfigapi/cli/azure.go deleted file mode 100644 index a10fb4e131..0000000000 --- a/internal/api/attestationconfigapi/cli/azure.go +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package main - -import ( - "context" - "fmt" - - "github.com/aws/aws-sdk-go-v2/service/s3" - s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/aws/aws-sdk-go/aws" - "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" - "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" - "github.com/edgelesssys/constellation/v2/internal/staticupload" -) - -func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { - if cfg.provider != cloudprovider.Azure && cfg.kind != snpReport { - return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) - } - - return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version) -} - -func deleteRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error { - resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: aws.String(cfg.bucket), - Prefix: aws.String(path), - }) - if err != nil { - return err - } - - // Delete all objects in the path. - objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents)) - for i, obj := range resp.Contents { - objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key} - } - if len(objIDs) > 0 { - _, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ - Bucket: aws.String(cfg.bucket), - Delete: &s3types.Delete{ - Objects: objIDs, - Quiet: toPtr(true), - }, - }) - if err != nil { - return err - } - } - return nil -} - -func toPtr[T any](v T) *T { - return &v -} diff --git a/internal/api/attestationconfigapi/cli/delete.go b/internal/api/attestationconfigapi/cli/delete.go index d0b0f447f6..daf4574156 100644 --- a/internal/api/attestationconfigapi/cli/delete.go +++ b/internal/api/attestationconfigapi/cli/delete.go @@ -6,11 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only package main import ( + "context" "errors" "fmt" "log/slog" "path" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" @@ -22,7 +26,7 @@ import ( // newDeleteCmd creates the delete command. func newDeleteCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "delete {azure|aws} {snp-report|guest-firmware} ", + Use: "delete {aws|azure|gcp} {snp-report|guest-firmware} ", Short: "Delete an object from the attestationconfig API", Long: "Delete a specific object version from the config api. is the name of the object to delete (without .json suffix)", Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete azure snp-report 1.0.0", @@ -32,7 +36,7 @@ func newDeleteCmd() *cobra.Command { } recursivelyCmd := &cobra.Command{ - Use: "recursive {azure|aws}", + Use: "recursive {aws|azure|gcp}", Short: "delete all objects from the API path constellation/v1/attestation/", Long: "Delete all objects from the API path constellation/v1/attestation/", Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete recursive azure", @@ -72,9 +76,11 @@ func runDelete(cmd *cobra.Command, args []string) (retErr error) { switch deleteCfg.provider { case cloudprovider.AWS: - return deleteAWS(cmd.Context(), client, deleteCfg) + return deleteEntry(cmd.Context(), variant.AWSSEVSNP{}, client, deleteCfg) case cloudprovider.Azure: - return deleteAzure(cmd.Context(), client, deleteCfg) + return deleteEntry(cmd.Context(), variant.AzureSEVSNP{}, client, deleteCfg) + case cloudprovider.GCP: + return deleteEntry(cmd.Context(), variant.GCPSEVSNP{}, client, deleteCfg) default: return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider) } @@ -111,11 +117,13 @@ func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) { deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AWSSEVSNP{}.String()) case cloudprovider.Azure: deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AzureSEVSNP{}.String()) + case cloudprovider.GCP: + deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.GCPSEVSNP{}.String()) default: return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider) } - return deleteRecursive(cmd.Context(), deletePath, client, deleteCfg) + return deleteEntryRecursive(cmd.Context(), deletePath, client, deleteCfg) } type deleteConfig struct { @@ -161,3 +169,44 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) { cosignPublicKey: apiCfg.cosignPublicKey, }, nil } + +func deleteEntry(ctx context.Context, attvar variant.Variant, client *attestationconfigapi.Client, cfg deleteConfig) error { + if cfg.kind != snpReport { + return fmt.Errorf("kind %s not supported", cfg.kind) + } + + return client.DeleteSEVSNPVersion(ctx, attvar, cfg.version) +} + +func deleteEntryRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error { + resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: aws.String(cfg.bucket), + Prefix: aws.String(path), + }) + if err != nil { + return err + } + + // Delete all objects in the path. + objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents)) + for i, obj := range resp.Contents { + objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key} + } + if len(objIDs) > 0 { + _, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: aws.String(cfg.bucket), + Delete: &s3types.Delete{ + Objects: objIDs, + Quiet: toPtr(true), + }, + }) + if err != nil { + return err + } + } + return nil +} + +func toPtr[T any](v T) *T { + return &v +} diff --git a/internal/api/attestationconfigapi/cli/e2e/test.sh.in b/internal/api/attestationconfigapi/cli/e2e/test.sh.in index 773443df45..5fb23f06f7 100755 --- a/internal/api/attestationconfigapi/cli/e2e/test.sh.in +++ b/internal/api/attestationconfigapi/cli/e2e/test.sh.in @@ -26,6 +26,9 @@ function variant() { elif [[ $1 == "azure" ]]; then echo "azure-sev-snp" return 0 + elif [[ $1 == "gcp" ]]; then + echo "gcp-sev-snp" + return 0 else echo "Unknown CSP: $1" exit 1 diff --git a/internal/api/attestationconfigapi/cli/upload.go b/internal/api/attestationconfigapi/cli/upload.go index 54036009ab..98303cae2d 100644 --- a/internal/api/attestationconfigapi/cli/upload.go +++ b/internal/api/attestationconfigapi/cli/upload.go @@ -26,7 +26,7 @@ import ( func newUploadCmd() *cobra.Command { uploadCmd := &cobra.Command{ - Use: "upload {azure|aws} {snp-report|guest-firmware} ", + Use: "upload {aws|azure|gcp} {snp-report|guest-firmware} ", Short: "Upload an object to the attestationconfig API", Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+ @@ -92,17 +92,19 @@ func runUpload(cmd *cobra.Command, args []string) (retErr error) { return fmt.Errorf("creating client: %w", err) } - var attesation variant.Variant + var attestation variant.Variant switch uploadCfg.provider { case cloudprovider.AWS: - attesation = variant.AWSSEVSNP{} + attestation = variant.AWSSEVSNP{} case cloudprovider.Azure: - attesation = variant.AzureSEVSNP{} + attestation = variant.AzureSEVSNP{} + case cloudprovider.GCP: + attestation = variant.GCPSEVSNP{} default: return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider) } - return uploadReport(ctx, attesation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) + return uploadReport(ctx, attestation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) } func uploadReport(ctx context.Context, diff --git a/internal/api/attestationconfigapi/client.go b/internal/api/attestationconfigapi/client.go index 583e3bba4d..11b27eae0b 100644 --- a/internal/api/attestationconfigapi/client.go +++ b/internal/api/attestationconfigapi/client.go @@ -48,7 +48,7 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK return repo, clientClose, nil } -// uploadSEVSNPVersion uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. +// uploadSEVSNPVersion uploads the latest version numbers of the SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { versions, err := a.List(ctx, attestation) if err != nil { @@ -75,7 +75,9 @@ func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Var // List returns the list of versions for the given attestation variant. func (a Client) List(ctx context.Context, attestation variant.Variant) (SEVSNPVersionList, error) { - if !attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{}) { + if !attestation.Equal(variant.AzureSEVSNP{}) && + !attestation.Equal(variant.AWSSEVSNP{}) && + !attestation.Equal(variant.GCPSEVSNP{}) { return SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation) } diff --git a/internal/api/attestationconfigapi/reporter.go b/internal/api/attestationconfigapi/reporter.go index 00656e8816..72a9803479 100644 --- a/internal/api/attestationconfigapi/reporter.go +++ b/internal/api/attestationconfigapi/reporter.go @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only */ /* -The reporter contains the logic to determine a latest version for Azure SEVSNP based on cached version values observed on CVM instances. +The reporter contains the logic to determine a latest version for SEVSNP based on cached version values observed on CVM instances. Some code in this file (e.g. listing cached files) does not rely on dedicated API objects and instead uses the AWS SDK directly, for no other reason than original development speed. */ @@ -79,11 +79,11 @@ func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation varia if err := c.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil { return fmt.Errorf("uploading version: %w", err) } - c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion)) + c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new SEV-SNP version: %+v", minVersion)) return nil } -// cacheSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API. +// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API. func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { dateStr := date.Format(VersionFormat) + ".json" res := putCmd{ diff --git a/internal/api/attestationconfigapi/snp.go b/internal/api/attestationconfigapi/snp.go index 68098a3ad6..a0f92700b6 100644 --- a/internal/api/attestationconfigapi/snp.go +++ b/internal/api/attestationconfigapi/snp.go @@ -19,15 +19,15 @@ import ( // AttestationURLPath is the URL path to the attestation versions. const AttestationURLPath = "constellation/v1/attestation" -// SEVSNPVersion tracks the latest version of each component of the Azure SEVSNP. +// SEVSNPVersion tracks the latest version of each component of the SEVSNP. type SEVSNPVersion struct { - // Bootloader is the latest version of the Azure SEVSNP bootloader. + // Bootloader is the latest version of the SEVSNP bootloader. Bootloader uint8 `json:"bootloader"` - // TEE is the latest version of the Azure SEVSNP TEE. + // TEE is the latest version of the SEVSNP TEE. TEE uint8 `json:"tee"` - // SNP is the latest version of the Azure SEVSNP SNP. + // SNP is the latest version of the SEVSNP SNP. SNP uint8 `json:"snp"` - // Microcode is the latest version of the Azure SEVSNP microcode. + // Microcode is the latest version of the SEVSNP microcode. Microcode uint8 `json:"microcode"` } diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go index 806baa2619..0ceb5e9601 100644 --- a/internal/attestation/gcp/snp/issuer.go +++ b/internal/attestation/gcp/snp/issuer.go @@ -62,8 +62,8 @@ func getInstanceInfo(_ context.Context, _ io.ReadWriteCloser, extraData []byte) if len(extraData) > 64 { return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) } - truncatedExtraData := make([]byte, 64) - copy(truncatedExtraData, extraData) + extraData64 := make([]byte, 64) + copy(extraData64, extraData) device, err := sevclient.OpenDevice() if err != nil { @@ -71,7 +71,7 @@ func getInstanceInfo(_ context.Context, _ io.ReadWriteCloser, extraData []byte) } defer device.Close() - report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, [64]byte(truncatedExtraData), 0) + report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, [64]byte(extraData64), 0) if err != nil { return nil, fmt.Errorf("getting extended report: %w", err) } diff --git a/internal/config/config.go b/internal/config/config.go index c7a7ec63c8..c3ea6b34de 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -476,6 +476,12 @@ func New(fileHandler file.Handler, name string, fetcher attestationconfigapi.Fet } } + if gcp := c.Attestation.GCPSEVSNP; gcp != nil { + if err := gcp.FetchAndSetLatestVersionNumbers(context.Background(), fetcher); err != nil { + return c, err + } + } + // Read secrets from env-vars. clientSecretValue := os.Getenv(constants.EnvVarAzureClientSecretValue) if clientSecretValue != "" && c.Provider.Azure != nil { diff --git a/internal/config/gcp.go b/internal/config/gcp.go index b4cc590c66..847474f05b 100644 --- a/internal/config/gcp.go +++ b/internal/config/gcp.go @@ -73,39 +73,35 @@ func (c *GCPSEVSNP) getToMarshallLatestWithResolvedVersions() AttestationCfg { } // FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. -func (c *GCPSEVSNP) FetchAndSetLatestVersionNumbers(_ context.Context, _ attestationconfigapi.Fetcher) error { - panic("not implemented") - - // TODO(msanft): Implement with https://dev.azure.com/Edgeless/Edgeless/_workitems/edit/4024 - - // // 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) 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 -// } -// } +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 { From 694c8b124f1c6891015704f963cfe41bab2b7a6e Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:03:38 +0200 Subject: [PATCH 28/30] snp: use correct cert Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --- internal/attestation/gcp/snp/issuer.go | 4 ++-- internal/attestation/gcp/snp/validator.go | 14 +++++++------- internal/attestation/snp/snp.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go index 0ceb5e9601..ab68c3edeb 100644 --- a/internal/attestation/gcp/snp/issuer.go +++ b/internal/attestation/gcp/snp/issuer.go @@ -62,8 +62,8 @@ func getInstanceInfo(_ context.Context, _ io.ReadWriteCloser, extraData []byte) if len(extraData) > 64 { return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) } - extraData64 := make([]byte, 64) - copy(extraData64, extraData) + var extraData64 [64]byte + copy(extraData64[:], extraData) device, err := sevclient.OpenDevice() if err != nil { diff --git a/internal/attestation/gcp/snp/validator.go b/internal/attestation/gcp/snp/validator.go index cd72366d7e..c7505778da 100644 --- a/internal/attestation/gcp/snp/validator.go +++ b/internal/attestation/gcp/snp/validator.go @@ -68,21 +68,21 @@ func NewValidator(cfg *config.GCPSEVSNP, log attestation.Logger) (*Validator, er // getTrustedKey returns TPM endorsement key provided through the GCE metadata API. func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, extraData []byte) (crypto.PublicKey, error) { - ekPub, err := v.gceKeyGetter(ctx, attDoc, nil) - if err != nil { - return nil, fmt.Errorf("getting TPM endorsement key: %w", err) - } - if len(extraData) > 64 { return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) } - extraData64 := make([]byte, 64) - copy(extraData64, extraData) + var extraData64 [64]byte + copy(extraData64[:], extraData) if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), [64]byte(extraData64), v.cfg, v.log); err != nil { return nil, fmt.Errorf("validating SNP report: %w", err) } + ekPub, err := v.gceKeyGetter(ctx, attDoc, nil) + if err != nil { + return nil, fmt.Errorf("getting TPM endorsement key: %w", err) + } + return ekPub, nil } diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index 4079aa78ee..c341e31fa9 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -137,7 +137,7 @@ func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter, logger.Info("Using cached ASK certificate") att.CertificateChain.AskCert = fallbackCerts.ask.Raw } - if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil { + if fallbackCerts.ark != nil { logger.Info("Using cached ARK certificate") att.CertificateChain.ArkCert = fallbackCerts.ark.Raw } From 8fb087d3642b7f91efd7d6d9a34593790d578bf2 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:38:45 +0200 Subject: [PATCH 29/30] terraform-provider: enable fetching of attestation config values for GCP SEV-SNP --- .../internal/provider/attestation_data_source.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/terraform-provider-constellation/internal/provider/attestation_data_source.go b/terraform-provider-constellation/internal/provider/attestation_data_source.go index 56815ae223..abf2921b69 100644 --- a/terraform-provider-constellation/internal/provider/attestation_data_source.go +++ b/terraform-provider-constellation/internal/provider/attestation_data_source.go @@ -163,7 +163,9 @@ func (d *AttestationDataSource) Read(ctx context.Context, req datasource.ReadReq insecureFetch := data.Insecure.ValueBool() snpVersions := attestationconfigapi.SEVSNPVersionAPI{} - if attestationVariant.Equal(variant.AzureSEVSNP{}) || attestationVariant.Equal(variant.AWSSEVSNP{}) { + if attestationVariant.Equal(variant.AzureSEVSNP{}) || + attestationVariant.Equal(variant.AWSSEVSNP{}) || + attestationVariant.Equal(variant.GCPSEVSNP{}) { snpVersions, err = d.fetcher.FetchSEVSNPVersionLatest(ctx, attestationVariant) if err != nil { resp.Diagnostics.AddError("Fetching SNP Version numbers", err.Error()) From 2129781e5d7d150e0bf2f1faa7f1f8ee84e82eed Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:10:51 +0200 Subject: [PATCH 30/30] linter fixes --- internal/attestation/gcp/snp/issuer.go | 2 +- internal/attestation/gcp/snp/validator.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go index ab68c3edeb..59c56e2f95 100644 --- a/internal/attestation/gcp/snp/issuer.go +++ b/internal/attestation/gcp/snp/issuer.go @@ -71,7 +71,7 @@ func getInstanceInfo(_ context.Context, _ io.ReadWriteCloser, extraData []byte) } defer device.Close() - report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, [64]byte(extraData64), 0) + report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, extraData64, 0) if err != nil { return nil, fmt.Errorf("getting extended report: %w", err) } diff --git a/internal/attestation/gcp/snp/validator.go b/internal/attestation/gcp/snp/validator.go index c7505778da..c178c14ea4 100644 --- a/internal/attestation/gcp/snp/validator.go +++ b/internal/attestation/gcp/snp/validator.go @@ -74,7 +74,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo var extraData64 [64]byte copy(extraData64[:], extraData) - if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), [64]byte(extraData64), v.cfg, v.log); err != nil { + if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), extraData64, v.cfg, v.log); err != nil { return nil, fmt.Errorf("validating SNP report: %w", err) }