From 913b09aeb85967f673a4b68882f54f7d3043754b Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:13:47 +0200 Subject: [PATCH] Support SEV-SNP on GCP (#3011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * terraform: enable creation of SEV-SNP VMs on GCP * variant: add SEV-SNP attestation variant * config: add SEV-SNP config options for GCP * measurements: add GCP SEV-SNP measurements * gcp: separate package for SEV-ES * attestation: add GCP SEV-SNP attestation logic * gcp: factor out common logic * choose: add GCP SEV-SNP * cli: add TF variable passthrough for GCP SEV-SNP variables * cli: support GCP SEV-SNP for `constellation verify` * Adjust usage of GCP SEV-SNP throughout codebase * ci: add GCP SEV-SNP * terraform-provider: support GCP SEV-SNP * docs: add GCP SEV-SNP reference * linter fixes * gcp: only run test with TPM simulator * gcp: remove nonsense test * Update cli/internal/cmd/verify.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update docs/docs/overview/clouds.md Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update terraform-provider-constellation/internal/provider/attestation_data_source_test.go Co-authored-by: Adrian Stobbe * linter fixes * terraform_provider: correctly pass down CC technology * config: mark attestationconfigapi as unimplemented * gcp: fix comments and typos * snp: use nonce and PK hash in SNP report * snp: ensure we never use ARK supplied by Issuer (#3025) * 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> * attestationconfigapi: add GCP to uploading * snp: use correct cert Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * terraform-provider: enable fetching of attestation config values for GCP SEV-SNP * linter fixes --------- Signed-off-by: Daniel Weiße Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> Co-authored-by: Adrian Stobbe --- .github/actions/e2e_verify/action.yml | 2 +- .github/actions/terraform_apply/action.yml | 3 + .../workflows/e2e-attestationconfigapi.yml | 2 +- .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 | 11 + .github/workflows/e2e-test-release.yml | 44 ++++ .../workflows/e2e-test-terraform-provider.yml | 5 +- .github/workflows/e2e-test-weekly.yml | 45 ++++ .github/workflows/e2e-test.yml | 1 + .github/workflows/e2e-upgrade.yml | 5 +- .github/workflows/on-release.yml | 1 + cli/internal/cloudcmd/tfvars.go | 7 + cli/internal/cmd/configgenerate_test.go | 9 + cli/internal/cmd/verify.go | 9 +- cli/internal/terraform/variables.go | 2 + cli/internal/terraform/variables_test.go | 2 + docs/docs/reference/cli.md | 2 +- .../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/aws/snp/validator.go | 4 +- internal/attestation/azure/snp/validator.go | 46 ++-- internal/attestation/choose/BUILD.bazel | 3 +- internal/attestation/choose/choose.go | 11 +- internal/attestation/choose/choose_test.go | 6 + internal/attestation/gcp/BUILD.bazel | 28 +-- 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/gcp.go | 35 --- internal/attestation/gcp/issuer.go | 87 -------- internal/attestation/gcp/metadata.go | 69 ++++++ internal/attestation/gcp/restclient.go | 101 +++++++++ internal/attestation/gcp/snp/BUILD.bazel | 29 +++ internal/attestation/gcp/snp/issuer.go | 168 ++++++++++++++ internal/attestation/gcp/snp/snp.go | 42 ++++ internal/attestation/gcp/snp/validator.go | 206 ++++++++++++++++++ internal/attestation/gcp/validator.go | 120 ---------- .../measurement-generator/generate.go | 6 +- .../attestation/measurements/measurements.go | 3 + .../measurements/measurements_enterprise.go | 1 + .../measurements/measurements_oss.go | 9 + internal/attestation/snp/BUILD.bazel | 2 +- internal/attestation/snp/snp.go | 23 +- internal/attestation/snp/snp_test.go | 43 ++-- internal/attestation/variant/variant.go | 23 +- internal/attestation/vtpm/attestation.go | 25 ++- internal/config/BUILD.bazel | 1 + internal/config/attestation.go | 2 + internal/config/attestation_test.go | 3 + internal/config/config.go | 63 ++++-- internal/config/config_doc.go | 75 ++++++- internal/config/config_test.go | 7 +- internal/config/gcp.go | 128 +++++++++++ internal/config/validation.go | 8 +- internal/constellation/state/state.go | 4 +- measurement-reader/cmd/main.go | 2 +- .../docs/data-sources/attestation.md | 2 + .../docs/data-sources/image.md | 1 + .../docs/resources/cluster.md | 1 + .../examples/full/gcp/main.tf | 2 + .../provider/attestation_data_source.go | 4 +- .../provider/attestation_data_source_test.go | 29 ++- .../internal/provider/convert.go | 11 + .../provider/image_data_source_test.go | 19 +- .../internal/provider/shared_attributes.go | 3 +- .../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 + .../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 + 90 files changed, 1623 insertions(+), 552 deletions(-) delete mode 100644 internal/api/attestationconfigapi/cli/aws.go delete mode 100644 internal/api/attestationconfigapi/cli/azure.go 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 create mode 100644 internal/attestation/gcp/metadata.go create mode 100644 internal/attestation/gcp/restclient.go 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/snp.go create mode 100644 internal/attestation/gcp/snp/validator.go delete mode 100644 internal/attestation/gcp/validator.go create mode 100644 internal/config/gcp.go diff --git a/.github/actions/e2e_verify/action.yml b/.github/actions/e2e_verify/action.yml index aca4fdceb..c52d02f43 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/actions/terraform_apply/action.yml b/.github/actions/terraform_apply/action.yml index f66b18ace..89361d14f 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/.github/workflows/e2e-attestationconfigapi.yml b/.github/workflows/e2e-attestationconfigapi.yml index a3605dafc..e02c1d4db 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-daily.yml b/.github/workflows/e2e-test-daily.yml index c36923a97..c2a4880ed 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 6e87bd30d..b9a27949c 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 94e790cbb..3338c1384 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 91a807e59..f2b77fd09 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: @@ -265,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 < 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 d0b0f447f..daf457415 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 773443df4..5fb23f06f 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 54036009a..98303cae2 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 583e3bba4..11b27eae0 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 00656e881..72a980347 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 68098a3ad..a0f92700b 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/aws/snp/validator.go b/internal/attestation/aws/snp/validator.go index 22d8b814b..873851c73 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 a4b58e4d4..d3563d06a 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/choose/BUILD.bazel b/internal/attestation/choose/BUILD.bazel index dfb1938e4..09bd9d2b9 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 3ce936085..7d0e48010 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 33ca1849e..31454d2c9 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{}, }, diff --git a/internal/attestation/gcp/BUILD.bazel b/internal/attestation/gcp/BUILD.bazel index 7cabb294b..8b8c24d8c 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/es/BUILD.bazel b/internal/attestation/gcp/es/BUILD.bazel new file mode 100644 index 000000000..a7d089412 --- /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 000000000..7a6dfe446 --- /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 000000000..bbee2f5c3 --- /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 4ad64c7a2..483662855 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 000000000..4177b6f0a --- /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 203809a4f..3fa35da7e 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.CVMRestClient, 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.CVMRestClient, error) { + return func(_ context.Context, _ ...option.ClientOption) (gcp.CVMRestClient, error) { return &fakeInstanceClient{ getIdentErr: getIdentErr, ident: ident, diff --git a/internal/attestation/gcp/gcp.go b/internal/attestation/gcp/gcp.go index 893b002a6..113222dda 100644 --- a/internal/attestation/gcp/gcp.go +++ b/internal/attestation/gcp/gcp.go @@ -6,40 +6,5 @@ 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 */ package gcp diff --git a/internal/attestation/gcp/issuer.go b/internal/attestation/gcp/issuer.go deleted file mode 100644 index 4dc36ba0d..000000000 --- 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/metadata.go b/internal/attestation/gcp/metadata.go new file mode 100644 index 000000000..5fdd7046b --- /dev/null +++ b/internal/attestation/gcp/metadata.go @@ -0,0 +1,69 @@ +/* +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{} + +// 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 new file mode 100644 index 000000000..1a9c277f3 --- /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) (CVMRestClient, error) { + c, err := compute.NewInstancesRESTClient(ctx, opts...) + if err != nil { + return nil, err + } + return &RESTClient{c}, nil +} + +// 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 +} + +// 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) (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) + 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 +} diff --git a/internal/attestation/gcp/snp/BUILD.bazel b/internal/attestation/gcp/snp/BUILD.bazel new file mode 100644 index 000000000..cef1ff9c8 --- /dev/null +++ b/internal/attestation/gcp/snp/BUILD.bazel @@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +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_tools//client", + "@com_github_google_go_tpm_tools//proto/attest", + ], +) diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go new file mode 100644 index 000000000..59c56e2f9 --- /dev/null +++ b/internal/attestation/gcp/snp/issuer.go @@ -0,0 +1,168 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "context" + "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, _ 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)) + } + var extraData64 [64]byte + copy(extraData64[:], extraData) + + 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, extraData64, 0) + if err != nil { + return nil, fmt.Errorf("getting extended report: %w", err) + } + + vcek, certChain, err := parseSNPCertTable(certs) + if err != nil { + return nil, fmt.Errorf("parsing vcek: %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, + CertChain: certChain, + 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 +} + +// 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 parseSNPCertTable(certs []byte) (vcekPEM []byte, certChain []byte, err error) { + certTable := abi.CertTable{} + if err := certTable.Unmarshal(certs); err != nil { + return nil, nil, fmt.Errorf("unmarshalling SNP certificate table: %w", err) + } + + vcekRaw, err := certTable.GetByGUIDString(abi.VcekGUID) + if err != nil { + return nil, nil, fmt.Errorf("getting VCEK certificate: %w", err) + } + + // An optional check for certificate well-formedness. vcekRaw == cert.Raw. + vcek, err := x509.ParseCertificate(vcekRaw) + if err != nil { + return nil, nil, fmt.Errorf("parsing certificate: %w", err) + } + + vcekPEM = pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: vcek.Raw, + }) + + 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/snp.go b/internal/attestation/gcp/snp/snp.go new file mode 100644 index 000000000..ede60f205 --- /dev/null +++ b/internal/attestation/gcp/snp/snp.go @@ -0,0 +1,42 @@ +/* +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. + +[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 new file mode 100644 index 000000000..c178c14ea --- /dev/null +++ b/internal/attestation/gcp/snp/validator.go @@ -0,0 +1,206 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "context" + "crypto" + "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" +) + +// 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("creating trusted key getter: %w", 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, + 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, extraData []byte) (crypto.PublicKey, error) { + if len(extraData) > 64 { + return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) + } + var extraData64 [64]byte + copy(extraData64[:], extraData) + + 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) + } + + ekPub, err := v.gceKeyGetter(ctx, attDoc, nil) + if err != nil { + return nil, fmt.Errorf("getting TPM endorsement key: %w", err) + } + + return ekPub, 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, 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) + } + + 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: 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 + // 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 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{ + 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/validator.go b/internal/attestation/gcp/validator.go deleted file mode 100644 index 310a33b55..000000000 --- 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 -} diff --git a/internal/attestation/measurements/measurement-generator/generate.go b/internal/attestation/measurements/measurement-generator/generate.go index b552c6b7d..bdb8e943f 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 a702706bd..1bea6174d 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 a3d09b049..b8e699012 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 552d6bd26..0ef7ce640 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/internal/attestation/snp/BUILD.bazel b/internal/attestation/snp/BUILD.bazel index 700a3aa86..f62518f25 100644 --- a/internal/attestation/snp/BUILD.bazel +++ b/internal/attestation/snp/BUILD.bazel @@ -8,11 +8,11 @@ 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", "@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 95cba55bf..c341e31fa 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -15,11 +15,11 @@ 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" "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 +39,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. @@ -95,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, @@ -120,30 +121,28 @@ 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 { logger.Info("Using cached ASK certificate") 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)) + if fallbackCerts.ark != nil { + 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 0179ac05b..2eaf3e52a 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": { diff --git a/internal/attestation/variant/variant.go b/internal/attestation/variant/variant.go index 43397a94b..e71a51480 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{} diff --git a/internal/attestation/vtpm/attestation.go b/internal/attestation/vtpm/attestation.go index 77c396b9a..364ab1163 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" @@ -123,12 +125,7 @@ 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) - 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 +133,14 @@ 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) + + // 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) + } + 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)) +} diff --git a/internal/config/BUILD.bazel b/internal/config/BUILD.bazel index c653c489c..8ea071ae8 100644 --- a/internal/config/BUILD.bazel +++ b/internal/config/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "azure.go", "config.go", "config_doc.go", + "gcp.go", # keep "image_enterprise.go", # keep diff --git a/internal/config/attestation.go b/internal/config/attestation.go index dc4d8fb83..f635ebbbd 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -52,6 +52,8 @@ func UnmarshalAttestationConfig(data []byte, attestVariant variant.Variant) (Att return unmarshalTypedConfig[*AzureTDX](data) case variant.GCPSEVES{}: return unmarshalTypedConfig[*GCPSEVES](data) + case variant.GCPSEVSNP{}: + return unmarshalTypedConfig[*GCPSEVSNP](data) case variant.QEMUVTPM{}: return unmarshalTypedConfig[*QEMUVTPM](data) case variant.QEMUTDX{}: diff --git a/internal/config/attestation_test.go b/internal/config/attestation_test.go index e0e3492dc..a690ba40b 100644 --- a/internal/config/attestation_test.go +++ b/internal/config/attestation_test.go @@ -41,6 +41,9 @@ func TestUnmarshalAttestationConfig(t *testing.T) { "GCPSEVES": { cfg: &GCPSEVES{Measurements: measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVES{})}, }, + "GCPSEVSNP": { + cfg: DefaultForGCPSEVSNP(), + }, "QEMUVTPM": { cfg: &QEMUVTPM{Measurements: measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUVTPM{})}, }, diff --git a/internal/config/config.go b/internal/config/config.go index 10ac013d1..c3ea6b34d 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{})}, }, } @@ -472,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 { @@ -518,6 +528,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 +583,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 +652,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 +982,29 @@ type GCPSEVES struct { Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` } -// GetVariant returns gcp-sev-es as the variant. -func (GCPSEVES) GetVariant() variant.Variant { - return variant.GCPSEVES{} -} - -// GetMeasurements returns the measurements used for attestation. -func (c GCPSEVES) GetMeasurements() measurements.M { - return c.Measurements -} - -// SetMeasurements updates a config's measurements using the given measurements. -func (c *GCPSEVES) SetMeasurements(m measurements.M) { - c.Measurements = m -} - -// EqualTo returns true if the config is equal to the given config. -func (c GCPSEVES) EqualTo(other AttestationCfg) (bool, error) { - otherCfg, ok := other.(*GCPSEVES) - if !ok { - return false, fmt.Errorf("cannot compare %T with %T", c, other) - } - return c.Measurements.EqualTo(otherCfg.Measurements), nil +// GCPSEVSNP is the configuration for GCP SEV-SNP attestation. +type GCPSEVSNP struct { + // description: | + // Expected TPM measurements. + Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` + // description: | + // Lowest acceptable bootloader version. + BootloaderVersion AttestationVersion `json:"bootloaderVersion" yaml:"bootloaderVersion"` + // description: | + // Lowest acceptable TEE version. + TEEVersion AttestationVersion `json:"teeVersion" yaml:"teeVersion"` + // description: | + // Lowest acceptable SEV-SNP version. + SNPVersion AttestationVersion `json:"snpVersion" yaml:"snpVersion"` + // description: | + // Lowest acceptable microcode version. + MicrocodeVersion AttestationVersion `json:"microcodeVersion" yaml:"microcodeVersion"` + // description: | + // AMD Root Key certificate used to verify the SEV-SNP certificate chain. + AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"` + // description: | + // AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate. + AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty"` } // QEMUVTPM is the configuration for QEMU vTPM attestation. diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index 2168b7f98..56f358d03 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -23,6 +23,7 @@ var ( UnsupportedAppRegistrationErrorDoc encoder.Doc SNPFirmwareSignerConfigDoc encoder.Doc GCPSEVESDoc encoder.Doc + GCPSEVSNPDoc encoder.Doc QEMUVTPMDoc encoder.Doc QEMUTDXDoc encoder.Doc AWSSEVSNPDoc encoder.Doc @@ -388,7 +389,7 @@ func init() { FieldName: "attestation", }, } - AttestationConfigDoc.Fields = make([]encoder.Doc, 8) + AttestationConfigDoc.Fields = make([]encoder.Doc, 9) AttestationConfigDoc.Fields[0].Name = "awsSEVSNP" AttestationConfigDoc.Fields[0].Type = "AWSSEVSNP" AttestationConfigDoc.Fields[0].Note = "" @@ -419,16 +420,21 @@ func init() { AttestationConfigDoc.Fields[5].Note = "" AttestationConfigDoc.Fields[5].Description = "GCP SEV-ES attestation." AttestationConfigDoc.Fields[5].Comments[encoder.LineComment] = "GCP SEV-ES attestation." - AttestationConfigDoc.Fields[6].Name = "qemuTDX" - AttestationConfigDoc.Fields[6].Type = "QEMUTDX" + AttestationConfigDoc.Fields[6].Name = "gcpSEVSNP" + AttestationConfigDoc.Fields[6].Type = "GCPSEVSNP" AttestationConfigDoc.Fields[6].Note = "" - AttestationConfigDoc.Fields[6].Description = "QEMU tdx attestation." - AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "QEMU tdx attestation." - AttestationConfigDoc.Fields[7].Name = "qemuVTPM" - AttestationConfigDoc.Fields[7].Type = "QEMUVTPM" + AttestationConfigDoc.Fields[6].Description = "description: |\n GCP SEV-SNP attestation.\n" + AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "description: |" + AttestationConfigDoc.Fields[7].Name = "qemuTDX" + AttestationConfigDoc.Fields[7].Type = "QEMUTDX" AttestationConfigDoc.Fields[7].Note = "" - AttestationConfigDoc.Fields[7].Description = "QEMU vTPM attestation." - AttestationConfigDoc.Fields[7].Comments[encoder.LineComment] = "QEMU vTPM attestation." + AttestationConfigDoc.Fields[7].Description = "QEMU tdx attestation." + AttestationConfigDoc.Fields[7].Comments[encoder.LineComment] = "QEMU tdx attestation." + AttestationConfigDoc.Fields[8].Name = "qemuVTPM" + AttestationConfigDoc.Fields[8].Type = "QEMUVTPM" + AttestationConfigDoc.Fields[8].Note = "" + AttestationConfigDoc.Fields[8].Description = "QEMU vTPM attestation." + AttestationConfigDoc.Fields[8].Comments[encoder.LineComment] = "QEMU vTPM attestation." NodeGroupDoc.Type = "NodeGroup" NodeGroupDoc.Comments[encoder.LineComment] = "NodeGroup defines a group of nodes with the same role and configuration." @@ -518,6 +524,52 @@ func init() { GCPSEVESDoc.Fields[0].Description = "Expected TPM measurements." GCPSEVESDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + GCPSEVSNPDoc.Type = "GCPSEVSNP" + GCPSEVSNPDoc.Comments[encoder.LineComment] = "GCPSEVSNP is the configuration for GCP SEV-SNP attestation." + GCPSEVSNPDoc.Description = "GCPSEVSNP is the configuration for GCP SEV-SNP attestation." + GCPSEVSNPDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "AttestationConfig", + FieldName: "gcpSEVSNP", + }, + } + GCPSEVSNPDoc.Fields = make([]encoder.Doc, 7) + GCPSEVSNPDoc.Fields[0].Name = "measurements" + GCPSEVSNPDoc.Fields[0].Type = "M" + GCPSEVSNPDoc.Fields[0].Note = "" + GCPSEVSNPDoc.Fields[0].Description = "Expected TPM measurements." + GCPSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + GCPSEVSNPDoc.Fields[1].Name = "bootloaderVersion" + GCPSEVSNPDoc.Fields[1].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[1].Note = "" + GCPSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version." + GCPSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version." + GCPSEVSNPDoc.Fields[2].Name = "teeVersion" + GCPSEVSNPDoc.Fields[2].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[2].Note = "" + GCPSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version." + GCPSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version." + GCPSEVSNPDoc.Fields[3].Name = "snpVersion" + GCPSEVSNPDoc.Fields[3].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[3].Note = "" + GCPSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version." + GCPSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version." + GCPSEVSNPDoc.Fields[4].Name = "microcodeVersion" + GCPSEVSNPDoc.Fields[4].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[4].Note = "" + GCPSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version." + GCPSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version." + GCPSEVSNPDoc.Fields[5].Name = "amdRootKey" + GCPSEVSNPDoc.Fields[5].Type = "Certificate" + GCPSEVSNPDoc.Fields[5].Note = "" + GCPSEVSNPDoc.Fields[5].Description = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + GCPSEVSNPDoc.Fields[5].Comments[encoder.LineComment] = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + GCPSEVSNPDoc.Fields[6].Name = "amdSigningKey" + GCPSEVSNPDoc.Fields[6].Type = "Certificate" + GCPSEVSNPDoc.Fields[6].Note = "" + GCPSEVSNPDoc.Fields[6].Description = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + GCPSEVSNPDoc.Fields[6].Comments[encoder.LineComment] = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + QEMUVTPMDoc.Type = "QEMUVTPM" QEMUVTPMDoc.Comments[encoder.LineComment] = "QEMUVTPM is the configuration for QEMU vTPM attestation." QEMUVTPMDoc.Description = "QEMUVTPM is the configuration for QEMU vTPM attestation." @@ -779,6 +831,10 @@ func (_ GCPSEVES) Doc() *encoder.Doc { return &GCPSEVESDoc } +func (_ GCPSEVSNP) Doc() *encoder.Doc { + return &GCPSEVSNPDoc +} + func (_ QEMUVTPM) Doc() *encoder.Doc { return &QEMUVTPMDoc } @@ -825,6 +881,7 @@ func GetConfigurationDoc() *encoder.FileDoc { &UnsupportedAppRegistrationErrorDoc, &SNPFirmwareSignerConfigDoc, &GCPSEVESDoc, + &GCPSEVSNPDoc, &QEMUVTPMDoc, &QEMUTDXDoc, &AWSSEVSNPDoc, diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 013c50edc..fa9c0c2d0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -328,7 +328,7 @@ func TestFromFile(t *testing.T) { } func TestValidate(t *testing.T) { - const defaultErrCount = 32 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default + const defaultErrCount = 33 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default const azErrCount = 7 const awsErrCount = 8 const gcpErrCount = 8 @@ -735,6 +735,11 @@ func TestValidInstanceTypeForProvider(t *testing.T) { instanceTypes: instancetypes.GCPInstanceTypes, expectedResult: true, }, + "gcp sev-snp": { + variant: variant.GCPSEVSNP{}, + instanceTypes: instancetypes.GCPInstanceTypes, + expectedResult: true, + }, "put gcp when azure is set": { variant: variant.AzureSEVSNP{}, instanceTypes: instancetypes.GCPInstanceTypes, diff --git a/internal/config/gcp.go b/internal/config/gcp.go new file mode 100644 index 000000000..847474f05 --- /dev/null +++ b/internal/config/gcp.go @@ -0,0 +1,128 @@ +/* +Copyright (c) Edgeless Systems GmbH +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package config + +import ( + "bytes" + "context" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" +) + +var _ svnResolveMarshaller = &GCPSEVSNP{} + +// DefaultForGCPSEVSNP provides a valid default configuration for GCP SEV-SNP attestation. +func DefaultForGCPSEVSNP() *GCPSEVSNP { + return &GCPSEVSNP{ + Measurements: measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVSNP{}), + BootloaderVersion: NewLatestPlaceholderVersion(), + TEEVersion: NewLatestPlaceholderVersion(), + SNPVersion: NewLatestPlaceholderVersion(), + MicrocodeVersion: NewLatestPlaceholderVersion(), + AMDRootKey: mustParsePEM(arkPEM), + } +} + +// GetVariant returns gcp-sev-snp as the variant. +func (GCPSEVSNP) GetVariant() variant.Variant { + return variant.GCPSEVSNP{} +} + +// GetMeasurements returns the measurements used for attestation. +func (c GCPSEVSNP) GetMeasurements() measurements.M { + return c.Measurements +} + +// SetMeasurements updates a config's measurements using the given measurements. +func (c *GCPSEVSNP) SetMeasurements(m measurements.M) { + c.Measurements = m +} + +// EqualTo returns true if the config is equal to the given config. +func (c GCPSEVSNP) EqualTo(other AttestationCfg) (bool, error) { + otherCfg, ok := other.(*GCPSEVSNP) + if !ok { + return false, fmt.Errorf("cannot compare %T with %T", c, other) + } + + measurementsEqual := c.Measurements.EqualTo(otherCfg.Measurements) + bootloaderEqual := c.BootloaderVersion == otherCfg.BootloaderVersion + teeEqual := c.TEEVersion == otherCfg.TEEVersion + snpEqual := c.SNPVersion == otherCfg.SNPVersion + microcodeEqual := c.MicrocodeVersion == otherCfg.MicrocodeVersion + rootKeyEqual := bytes.Equal(c.AMDRootKey.Raw, otherCfg.AMDRootKey.Raw) + signingKeyEqual := bytes.Equal(c.AMDSigningKey.Raw, otherCfg.AMDSigningKey.Raw) + + return measurementsEqual && bootloaderEqual && teeEqual && snpEqual && microcodeEqual && rootKeyEqual && signingKeyEqual, nil +} + +func (c *GCPSEVSNP) getToMarshallLatestWithResolvedVersions() AttestationCfg { + cp := *c + cp.BootloaderVersion.WantLatest = false + cp.TEEVersion.WantLatest = false + cp.SNPVersion.WantLatest = false + cp.MicrocodeVersion.WantLatest = false + return &cp +} + +// FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. +func (c *GCPSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error { + // Only talk to the API if at least one version number is set to latest. + if !(c.BootloaderVersion.WantLatest || c.TEEVersion.WantLatest || c.SNPVersion.WantLatest || c.MicrocodeVersion.WantLatest) { + return nil + } + + versions, err := fetcher.FetchSEVSNPVersionLatest(ctx, variant.GCPSEVSNP{}) + if err != nil { + return fmt.Errorf("fetching latest TCB versions from configapi: %w", err) + } + // set number and keep isLatest flag + c.mergeWithLatestVersion(versions.SEVSNPVersion) + return nil +} + +func (c *GCPSEVSNP) mergeWithLatestVersion(latest attestationconfigapi.SEVSNPVersion) { + if c.BootloaderVersion.WantLatest { + c.BootloaderVersion.Value = latest.Bootloader + } + if c.TEEVersion.WantLatest { + c.TEEVersion.Value = latest.TEE + } + if c.SNPVersion.WantLatest { + c.SNPVersion.Value = latest.SNP + } + if c.MicrocodeVersion.WantLatest { + c.MicrocodeVersion.Value = latest.Microcode + } +} + +// GetVariant returns gcp-sev-es as the variant. +func (GCPSEVES) GetVariant() variant.Variant { + return variant.GCPSEVES{} +} + +// GetMeasurements returns the measurements used for attestation. +func (c GCPSEVES) GetMeasurements() measurements.M { + return c.Measurements +} + +// SetMeasurements updates a config's measurements using the given measurements. +func (c *GCPSEVES) SetMeasurements(m measurements.M) { + c.Measurements = m +} + +// EqualTo returns true if the config is equal to the given config. +func (c GCPSEVES) EqualTo(other AttestationCfg) (bool, error) { + otherCfg, ok := other.(*GCPSEVES) + if !ok { + return false, fmt.Errorf("cannot compare %T with %T", c, other) + } + return c.Measurements.EqualTo(otherCfg.Measurements), nil +} diff --git a/internal/config/validation.go b/internal/config/validation.go index 5e0ef59ee..fab69ff29 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -202,6 +202,9 @@ func validateAttestation(sl validator.StructLevel) { if attestation.GCPSEVES != nil { attestationCount++ } + if attestation.GCPSEVSNP != nil { + attestationCount++ + } if attestation.QEMUVTPM != nil { attestationCount++ } @@ -364,6 +367,9 @@ func (c *Config) translateMoreThanOneAttestationError(ut ut.Translator, fe valid if c.Attestation.GCPSEVES != nil { definedAttestations = append(definedAttestations, "GCPSEVES") } + if c.Attestation.GCPSEVSNP != nil { + definedAttestations = append(definedAttestations, "GCPSEVSNP") + } if c.Attestation.QEMUVTPM != nil { definedAttestations = append(definedAttestations, "QEMUVTPM") } @@ -536,7 +542,7 @@ func validInstanceTypeForProvider(insType string, attestation variant.Variant) b return true } } - case variant.GCPSEVES{}: + case variant.GCPSEVES{}, variant.GCPSEVSNP{}: for _, instanceType := range instancetypes.GCPInstanceTypes { if insType == instanceType { return true diff --git a/internal/constellation/state/state.go b/internal/constellation/state/state.go index bee5f8b2b..68e9b2845 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/measurement-reader/cmd/main.go b/measurement-reader/cmd/main.go index 15ce68d2e..9bdc44332 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") diff --git a/terraform-provider-constellation/docs/data-sources/attestation.md b/terraform-provider-constellation/docs/data-sources/attestation.md index ec4118c0f..b1b8891c0 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 7f7186b56..f0b37455a 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 7b6d1ca21..cf77d1f74 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: diff --git a/terraform-provider-constellation/examples/full/gcp/main.tf b/terraform-provider-constellation/examples/full/gcp/main.tf index f7ac80b04..1cbbd525c 100644 --- a/terraform-provider-constellation/examples/full/gcp/main.tf +++ b/terraform-provider-constellation/examples/full/gcp/main.tf @@ -24,6 +24,7 @@ locals { control_plane_count = 3 worker_count = 2 instance_type = "n2d-standard-4" + cc_technology = "SEV" master_secret = random_bytes.master_secret.hex master_secret_salt = random_bytes.master_secret_salt.hex @@ -79,6 +80,7 @@ module "gcp_infrastructure" { region = local.region project = local.project_id internal_load_balancer = false + cc_technology = local.cc_technology } data "constellation_attestation" "foo" { diff --git a/terraform-provider-constellation/internal/provider/attestation_data_source.go b/terraform-provider-constellation/internal/provider/attestation_data_source.go index 56815ae22..abf2921b6 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()) 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 4fed9fbe3..3740e6b11 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-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"), + // 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/convert.go b/terraform-provider-constellation/internal/provider/convert.go index 087728168..cfe9ec7fa 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 { 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 669899e39..986ee1b53 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 b6f96cd17..0a8c6a72d 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"), }, } } diff --git a/terraform/infrastructure/gcp/.terraform.lock.hcl b/terraform/infrastructure/gcp/.terraform.lock.hcl index bc58c4246..557993381 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 f38195522..33c359b68 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 2681c4d47..fe9da14ae 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 f4b9a7cdb..5370ec7d1 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 2589ba1be..263ee12a3 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 a0a2e4c4f..c1929792b 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 0a5074f53..5c7bab447 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 add9eeffa..5d158c9ad 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'." + } +} diff --git a/terraform/infrastructure/iam/gcp/.terraform.lock.hcl b/terraform/infrastructure/iam/gcp/.terraform.lock.hcl index 2fcd905b4..3575f3cfe 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 899d448c9..38afbe1ca 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 27c45b1ea..3029f1fb3 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 92787bfd4..0087b4fba 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'." + } +}