diff --git a/.github/actions/e2e_verify/action.yml b/.github/actions/e2e_verify/action.yml index aca4fdceb4..c52d02f43b 100644 --- a/.github/actions/e2e_verify/action.yml +++ b/.github/actions/e2e_verify/action.yml @@ -84,7 +84,7 @@ runs: aws-region: eu-central-1 - name: Upload extracted TCBs - if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'aws-sev-snp') + if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'aws-sev-snp' || inputs.attestationVariant == 'gcp-sev-snp') shell: bash env: COSIGN_PASSWORD: ${{ inputs.cosignPassword }} diff --git a/.github/actions/terraform_apply/action.yml b/.github/actions/terraform_apply/action.yml index f66b18ace0..89361d14f4 100644 --- a/.github/actions/terraform_apply/action.yml +++ b/.github/actions/terraform_apply/action.yml @@ -26,6 +26,9 @@ runs: "gcpSEVES") attestationVariant="gcp-sev-es" ;; + "gcpSEVSNP") + attestationVariant="gcp-sev-snp" + ;; *) echo "Unknown attestation variant: $(yq '.attestation | keys | .[0]' constellation-conf.yaml)" exit 1 diff --git a/.github/workflows/e2e-attestationconfigapi.yml b/.github/workflows/e2e-attestationconfigapi.yml index a3605dafce..e02c1d4db1 100644 --- a/.github/workflows/e2e-attestationconfigapi.yml +++ b/.github/workflows/e2e-attestationconfigapi.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false max-parallel: 1 matrix: - csp: ["azure", "aws"] + csp: ["azure", "aws", "gcp"] runs-on: ubuntu-22.04 permissions: id-token: write diff --git a/.github/workflows/e2e-test-daily.yml b/.github/workflows/e2e-test-daily.yml index c36923a971..c2a4880ed4 100644 --- a/.github/workflows/e2e-test-daily.yml +++ b/.github/workflows/e2e-test-daily.yml @@ -46,7 +46,7 @@ jobs: max-parallel: 5 matrix: kubernetesVersion: ["1.28"] # should be default - attestationVariant: ["gcp-sev-es", "azure-sev-snp", "azure-tdx", "aws-sev-snp"] + attestationVariant: ["gcp-sev-es", "gcp-sev-snp", "azure-sev-snp", "azure-tdx", "aws-sev-snp"] refStream: ["ref/main/stream/debug/?", "ref/release/stream/stable/?"] test: ["sonobuoy quick"] runs-on: ubuntu-22.04 diff --git a/.github/workflows/e2e-test-internal-lb.yml b/.github/workflows/e2e-test-internal-lb.yml index 6e87bd30da..b9a27949c3 100644 --- a/.github/workflows/e2e-test-internal-lb.yml +++ b/.github/workflows/e2e-test-internal-lb.yml @@ -11,10 +11,11 @@ on: description: "Which attestation variant to use." type: choice options: - - "gcp-sev-es" + - "aws-sev-snp" - "azure-sev-snp" - "azure-tdx" - - "aws-sev-snp" + - "gcp-sev-es" + - "gcp-sev-snp" default: "azure-sev-snp" required: true runner: diff --git a/.github/workflows/e2e-test-marketplace-image.yml b/.github/workflows/e2e-test-marketplace-image.yml index 94e790cbbd..3338c13847 100644 --- a/.github/workflows/e2e-test-marketplace-image.yml +++ b/.github/workflows/e2e-test-marketplace-image.yml @@ -11,10 +11,11 @@ on: description: "Which attestation variant to use." type: choice options: - - "gcp-sev-es" + - "aws-sev-snp" - "azure-sev-snp" - "azure-tdx" - - "aws-sev-snp" + - "gcp-sev-es" + - "gcp-sev-snp" default: "azure-sev-snp" required: true runner: diff --git a/.github/workflows/e2e-test-provider-example.yml b/.github/workflows/e2e-test-provider-example.yml index 91a807e593..f2b77fd099 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 d0b0f447f6..daf4574156 100644 --- a/internal/api/attestationconfigapi/cli/delete.go +++ b/internal/api/attestationconfigapi/cli/delete.go @@ -6,11 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only package main import ( + "context" "errors" "fmt" "log/slog" "path" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" @@ -22,7 +26,7 @@ import ( // newDeleteCmd creates the delete command. func newDeleteCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "delete {azure|aws} {snp-report|guest-firmware} ", + Use: "delete {aws|azure|gcp} {snp-report|guest-firmware} ", Short: "Delete an object from the attestationconfig API", Long: "Delete a specific object version from the config api. is the name of the object to delete (without .json suffix)", Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete azure snp-report 1.0.0", @@ -32,7 +36,7 @@ func newDeleteCmd() *cobra.Command { } recursivelyCmd := &cobra.Command{ - Use: "recursive {azure|aws}", + Use: "recursive {aws|azure|gcp}", Short: "delete all objects from the API path constellation/v1/attestation/", Long: "Delete all objects from the API path constellation/v1/attestation/", Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete recursive azure", @@ -72,9 +76,11 @@ func runDelete(cmd *cobra.Command, args []string) (retErr error) { switch deleteCfg.provider { case cloudprovider.AWS: - return deleteAWS(cmd.Context(), client, deleteCfg) + return deleteEntry(cmd.Context(), variant.AWSSEVSNP{}, client, deleteCfg) case cloudprovider.Azure: - return deleteAzure(cmd.Context(), client, deleteCfg) + return deleteEntry(cmd.Context(), variant.AzureSEVSNP{}, client, deleteCfg) + case cloudprovider.GCP: + return deleteEntry(cmd.Context(), variant.GCPSEVSNP{}, client, deleteCfg) default: return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider) } @@ -111,11 +117,13 @@ func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) { deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AWSSEVSNP{}.String()) case cloudprovider.Azure: deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AzureSEVSNP{}.String()) + case cloudprovider.GCP: + deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.GCPSEVSNP{}.String()) default: return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider) } - return deleteRecursive(cmd.Context(), deletePath, client, deleteCfg) + return deleteEntryRecursive(cmd.Context(), deletePath, client, deleteCfg) } type deleteConfig struct { @@ -161,3 +169,44 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) { cosignPublicKey: apiCfg.cosignPublicKey, }, nil } + +func deleteEntry(ctx context.Context, attvar variant.Variant, client *attestationconfigapi.Client, cfg deleteConfig) error { + if cfg.kind != snpReport { + return fmt.Errorf("kind %s not supported", cfg.kind) + } + + return client.DeleteSEVSNPVersion(ctx, attvar, cfg.version) +} + +func deleteEntryRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error { + resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: aws.String(cfg.bucket), + Prefix: aws.String(path), + }) + if err != nil { + return err + } + + // Delete all objects in the path. + objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents)) + for i, obj := range resp.Contents { + objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key} + } + if len(objIDs) > 0 { + _, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: aws.String(cfg.bucket), + Delete: &s3types.Delete{ + Objects: objIDs, + Quiet: toPtr(true), + }, + }) + if err != nil { + return err + } + } + return nil +} + +func toPtr[T any](v T) *T { + return &v +} diff --git a/internal/api/attestationconfigapi/cli/e2e/test.sh.in b/internal/api/attestationconfigapi/cli/e2e/test.sh.in index 773443df45..5fb23f06f7 100755 --- a/internal/api/attestationconfigapi/cli/e2e/test.sh.in +++ b/internal/api/attestationconfigapi/cli/e2e/test.sh.in @@ -26,6 +26,9 @@ function variant() { elif [[ $1 == "azure" ]]; then echo "azure-sev-snp" return 0 + elif [[ $1 == "gcp" ]]; then + echo "gcp-sev-snp" + return 0 else echo "Unknown CSP: $1" exit 1 diff --git a/internal/api/attestationconfigapi/cli/upload.go b/internal/api/attestationconfigapi/cli/upload.go index 54036009ab..98303cae2d 100644 --- a/internal/api/attestationconfigapi/cli/upload.go +++ b/internal/api/attestationconfigapi/cli/upload.go @@ -26,7 +26,7 @@ import ( func newUploadCmd() *cobra.Command { uploadCmd := &cobra.Command{ - Use: "upload {azure|aws} {snp-report|guest-firmware} ", + Use: "upload {aws|azure|gcp} {snp-report|guest-firmware} ", Short: "Upload an object to the attestationconfig API", Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+ @@ -92,17 +92,19 @@ func runUpload(cmd *cobra.Command, args []string) (retErr error) { return fmt.Errorf("creating client: %w", err) } - var attesation variant.Variant + var attestation variant.Variant switch uploadCfg.provider { case cloudprovider.AWS: - attesation = variant.AWSSEVSNP{} + attestation = variant.AWSSEVSNP{} case cloudprovider.Azure: - attesation = variant.AzureSEVSNP{} + attestation = variant.AzureSEVSNP{} + case cloudprovider.GCP: + attestation = variant.GCPSEVSNP{} default: return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider) } - return uploadReport(ctx, attesation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) + return uploadReport(ctx, attestation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) } func uploadReport(ctx context.Context, diff --git a/internal/api/attestationconfigapi/client.go b/internal/api/attestationconfigapi/client.go index 583e3bba4d..11b27eae0b 100644 --- a/internal/api/attestationconfigapi/client.go +++ b/internal/api/attestationconfigapi/client.go @@ -48,7 +48,7 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK return repo, clientClose, nil } -// uploadSEVSNPVersion uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. +// uploadSEVSNPVersion uploads the latest version numbers of the SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { versions, err := a.List(ctx, attestation) if err != nil { @@ -75,7 +75,9 @@ func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Var // List returns the list of versions for the given attestation variant. func (a Client) List(ctx context.Context, attestation variant.Variant) (SEVSNPVersionList, error) { - if !attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{}) { + if !attestation.Equal(variant.AzureSEVSNP{}) && + !attestation.Equal(variant.AWSSEVSNP{}) && + !attestation.Equal(variant.GCPSEVSNP{}) { return SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation) } diff --git a/internal/api/attestationconfigapi/reporter.go b/internal/api/attestationconfigapi/reporter.go index 00656e8816..72a9803479 100644 --- a/internal/api/attestationconfigapi/reporter.go +++ b/internal/api/attestationconfigapi/reporter.go @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only */ /* -The reporter contains the logic to determine a latest version for Azure SEVSNP based on cached version values observed on CVM instances. +The reporter contains the logic to determine a latest version for SEVSNP based on cached version values observed on CVM instances. Some code in this file (e.g. listing cached files) does not rely on dedicated API objects and instead uses the AWS SDK directly, for no other reason than original development speed. */ @@ -79,11 +79,11 @@ func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation varia if err := c.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil { return fmt.Errorf("uploading version: %w", err) } - c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion)) + c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new SEV-SNP version: %+v", minVersion)) return nil } -// cacheSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API. +// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API. func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { dateStr := date.Format(VersionFormat) + ".json" res := putCmd{ diff --git a/internal/api/attestationconfigapi/snp.go b/internal/api/attestationconfigapi/snp.go index 68098a3ad6..a0f92700b6 100644 --- a/internal/api/attestationconfigapi/snp.go +++ b/internal/api/attestationconfigapi/snp.go @@ -19,15 +19,15 @@ import ( // AttestationURLPath is the URL path to the attestation versions. const AttestationURLPath = "constellation/v1/attestation" -// SEVSNPVersion tracks the latest version of each component of the Azure SEVSNP. +// SEVSNPVersion tracks the latest version of each component of the SEVSNP. type SEVSNPVersion struct { - // Bootloader is the latest version of the Azure SEVSNP bootloader. + // Bootloader is the latest version of the SEVSNP bootloader. Bootloader uint8 `json:"bootloader"` - // TEE is the latest version of the Azure SEVSNP TEE. + // TEE is the latest version of the SEVSNP TEE. TEE uint8 `json:"tee"` - // SNP is the latest version of the Azure SEVSNP SNP. + // SNP is the latest version of the SEVSNP SNP. SNP uint8 `json:"snp"` - // Microcode is the latest version of the Azure SEVSNP microcode. + // Microcode is the latest version of the SEVSNP microcode. Microcode uint8 `json:"microcode"` } diff --git a/internal/attestation/aws/snp/validator.go b/internal/attestation/aws/snp/validator.go index 22d8b814b1..873851c73b 100644 --- a/internal/attestation/aws/snp/validator.go +++ b/internal/attestation/aws/snp/validator.go @@ -191,11 +191,11 @@ func (a *awsValidator) validate(attestation vtpm.AttestationDocument, ask *x509. func getVerifyOpts(att *sevsnp.Attestation) (*verify.Options, error) { ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) if err != nil { - return &verify.Options{}, fmt.Errorf("parsing VLEK certificate: %w", err) + return nil, fmt.Errorf("parsing ASK certificate: %w", err) } ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) if err != nil { - return &verify.Options{}, fmt.Errorf("parsing VLEK certificate: %w", err) + return nil, fmt.Errorf("parsing ARK certificate: %w", err) } verifyOpts := &verify.Options{ diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index a4b58e4d40..d3563d06ac 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -116,25 +116,11 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo return nil, fmt.Errorf("parsing attestation report: %w", err) } - // ASK, as cached in joinservice or reported from THIM / KDS. - ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) + verifyOpts, err := getVerifyOpts(att) if err != nil { - return nil, fmt.Errorf("parsing ASK certificate: %w", err) + return nil, fmt.Errorf("getting verify options: %w", err) } - verifyOpts := &verify.Options{ - TrustedRoots: map[string][]*trust.AMDRootCerts{ - "Milan": { - { - Product: "Milan", - ProductCerts: &trust.ProductCerts{ - Ask: ask, - Ark: trustedArk, - }, - }, - }, - }, - } if err := v.attestationVerifier.SNPAttestation(att, verifyOpts); err != nil { return nil, fmt.Errorf("verifying SNP attestation: %w", err) } @@ -252,3 +238,31 @@ type maaValidator interface { type hclAkValidator interface { Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error } + +func getVerifyOpts(att *spb.Attestation) (*verify.Options, error) { + // ASK, as cached in joinservice or reported from THIM / KDS. + ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) + if err != nil { + return nil, fmt.Errorf("parsing ASK certificate: %w", err) + } + ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) + if err != nil { + return nil, fmt.Errorf("parsing ARK certificate: %w", err) + } + + verifyOpts := &verify.Options{ + TrustedRoots: map[string][]*trust.AMDRootCerts{ + "Milan": { + { + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + Ask: ask, + Ark: ark, + }, + }, + }, + }, + } + + return verifyOpts, nil +} diff --git a/internal/attestation/choose/BUILD.bazel b/internal/attestation/choose/BUILD.bazel index dfb1938e42..09bd9d2b95 100644 --- a/internal/attestation/choose/BUILD.bazel +++ b/internal/attestation/choose/BUILD.bazel @@ -14,7 +14,8 @@ go_library( "//internal/attestation/azure/snp", "//internal/attestation/azure/tdx", "//internal/attestation/azure/trustedlaunch", - "//internal/attestation/gcp", + "//internal/attestation/gcp/es", + "//internal/attestation/gcp/snp", "//internal/attestation/qemu", "//internal/attestation/tdx", "//internal/attestation/variant", diff --git a/internal/attestation/choose/choose.go b/internal/attestation/choose/choose.go index 3ce936085f..7d0e480104 100644 --- a/internal/attestation/choose/choose.go +++ b/internal/attestation/choose/choose.go @@ -16,7 +16,8 @@ import ( azuresnp "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp" azuretdx "github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx" "github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch" - "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/es" + gcpsnp "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/qemu" "github.com/edgelesssys/constellation/v2/internal/attestation/tdx" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" @@ -37,7 +38,9 @@ func Issuer(attestationVariant variant.Variant, log attestation.Logger) (atls.Is case variant.AzureTDX{}: return azuretdx.NewIssuer(log), nil case variant.GCPSEVES{}: - return gcp.NewIssuer(log), nil + return es.NewIssuer(log), nil + case variant.GCPSEVSNP{}: + return gcpsnp.NewIssuer(log), nil case variant.QEMUVTPM{}: return qemu.NewIssuer(log), nil case variant.QEMUTDX{}: @@ -63,7 +66,9 @@ func Validator(cfg config.AttestationCfg, log attestation.Logger) (atls.Validato case *config.AzureTDX: return azuretdx.NewValidator(cfg, log), nil case *config.GCPSEVES: - return gcp.NewValidator(cfg, log), nil + return es.NewValidator(cfg, log) + case *config.GCPSEVSNP: + return gcpsnp.NewValidator(cfg, log) case *config.QEMUVTPM: return qemu.NewValidator(cfg, log), nil case *config.QEMUTDX: diff --git a/internal/attestation/choose/choose_test.go b/internal/attestation/choose/choose_test.go index 33ca1849e7..31454d2c92 100644 --- a/internal/attestation/choose/choose_test.go +++ b/internal/attestation/choose/choose_test.go @@ -40,6 +40,9 @@ func TestIssuer(t *testing.T) { "gcp-sev-es": { variant: variant.GCPSEVES{}, }, + "gcp-sev-snp": { + variant: variant.GCPSEVSNP{}, + }, "qemu-vtpm": { variant: variant.QEMUVTPM{}, }, @@ -89,6 +92,9 @@ func TestValidator(t *testing.T) { "gcp-sev-es": { cfg: &config.GCPSEVES{}, }, + "gcp-sev-snp": { + cfg: &config.GCPSEVSNP{}, + }, "qemu-vtpm": { cfg: &config.QEMUVTPM{}, }, diff --git a/internal/attestation/gcp/BUILD.bazel b/internal/attestation/gcp/BUILD.bazel index 7cabb294b2..8b8c24d8ca 100644 --- a/internal/attestation/gcp/BUILD.bazel +++ b/internal/attestation/gcp/BUILD.bazel @@ -1,21 +1,18 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//bazel/go:go_test.bzl", "go_test") go_library( name = "gcp", srcs = [ "gcp.go", - "issuer.go", - "validator.go", + "metadata.go", + "restclient.go", ], importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/gcp", visibility = ["//:__subpackages__"], deps = [ - "//internal/attestation", + "//internal/attestation/snp", "//internal/attestation/variant", "//internal/attestation/vtpm", - "//internal/config", - "@com_github_google_go_tpm_tools//client", "@com_github_google_go_tpm_tools//proto/attest", "@com_github_googleapis_gax_go_v2//:gax-go", "@com_google_cloud_go_compute//apiv1", @@ -24,22 +21,3 @@ go_library( "@org_golang_google_api//option", ], ) - -go_test( - name = "gcp_test", - srcs = [ - "issuer_test.go", - "validator_test.go", - ], - embed = [":gcp"], - deps = [ - "//internal/attestation/vtpm", - "@com_github_google_go_tpm_tools//proto/attest", - "@com_github_googleapis_gax_go_v2//:gax-go", - "@com_github_stretchr_testify//assert", - "@com_github_stretchr_testify//require", - "@com_google_cloud_go_compute//apiv1/computepb", - "@org_golang_google_api//option", - "@org_golang_google_protobuf//proto", - ], -) diff --git a/internal/attestation/gcp/es/BUILD.bazel b/internal/attestation/gcp/es/BUILD.bazel new file mode 100644 index 0000000000..a7d089412b --- /dev/null +++ b/internal/attestation/gcp/es/BUILD.bazel @@ -0,0 +1,43 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "es", + srcs = [ + "es.go", + "issuer.go", + "validator.go", + ], + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/es", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/attestation", + "//internal/attestation/gcp", + "//internal/attestation/variant", + "//internal/attestation/vtpm", + "//internal/config", + "@com_github_google_go_tpm_tools//client", + "@com_github_google_go_tpm_tools//proto/attest", + ], +) + +go_test( + name = "es_test", + srcs = [ + "issuer_test.go", + "validator_test.go", + ], + embed = [":es"], + deps = [ + "//internal/attestation/gcp", + "//internal/attestation/variant", + "//internal/attestation/vtpm", + "@com_github_google_go_tpm_tools//proto/attest", + "@com_github_googleapis_gax_go_v2//:gax-go", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + "@com_google_cloud_go_compute//apiv1/computepb", + "@org_golang_google_api//option", + "@org_golang_google_protobuf//proto", + ], +) diff --git a/internal/attestation/gcp/es/es.go b/internal/attestation/gcp/es/es.go new file mode 100644 index 0000000000..7a6dfe4464 --- /dev/null +++ b/internal/attestation/gcp/es/es.go @@ -0,0 +1,45 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +# GCP SEV-ES attestation + +Google offers [confidential VMs], utilizing AMD SEV-ES to provide memory encryption. + +AMD SEV-ES doesn't offer much in terms of remote attestation, and following that the VMs don't offer much either, see [their docs] on how to validate a confidential VM for some insights. +However, each VM comes with a [virtual Trusted Platform Module (vTPM)]. +This module can be used to generate VM unique encryption keys or to attest the platform's chain of boot. We can use the vTPM to verify the VM is running on AMD SEV-ES enabled hardware, allowing us to bootstrap a constellation cluster. + +# Issuer + +Generates a TPM attestation key using a Google provided attestation key. +Additionally project ID, zone, and instance name are fetched from the metadata server and attached to the attestation document. + +# Validator + +Verifies the TPM attestation by using a public key provided by Google's API corresponding to the project ID, zone, instance name tuple attached to the attestation document. + +# Problems + + - SEV-ES is somewhat limited when compared to the newer version SEV-SNP + + Comparison of SEV, SEV-ES, and SEV-SNP can be seen on page seven of [AMD's SNP whitepaper] + + - We have to trust Google + + Since the vTPM is provided by Google, and they could do whatever they want with it, we have no save proof of the VMs actually being confidential. + + - The provided vTPM has no endorsement certificate for its attestation key + + Without a certificate signing the authenticity of any endorsement keys we have no way of establishing a chain of trust. + Instead, we have to rely on Google's API to provide us with the public key of the vTPM's endorsement key. + +[confidential VMs]: https://cloud.google.com/compute/confidential-vm/docs/about-cvm +[their docs]: https://cloud.google.com/compute/confidential-vm/docs/monitoring +[virtual Trusted Platform Module (vTPM)]: https://cloud.google.com/security/shielded-cloud/shielded-vm#vtpm +[AMD's SNP whitepaper]: https://www.amd.com/system/files/TechDocs/SEV-SNP-strengthening-vm-isolation-with-integrity-protection-and-more.pdf#page=7 +*/ +package es diff --git a/internal/attestation/gcp/es/issuer.go b/internal/attestation/gcp/es/issuer.go new file mode 100644 index 0000000000..bbee2f5c39 --- /dev/null +++ b/internal/attestation/gcp/es/issuer.go @@ -0,0 +1,33 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package es + +import ( + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + tpmclient "github.com/google/go-tpm-tools/client" +) + +// Issuer for GCP confidential VM attestation. +type Issuer struct { + variant.GCPSEVES + *vtpm.Issuer +} + +// NewIssuer initializes a new GCP Issuer. +func NewIssuer(log attestation.Logger) *Issuer { + return &Issuer{ + Issuer: vtpm.NewIssuer( + vtpm.OpenVTPM, + tpmclient.GceAttestationKeyRSA, + gcp.GCEInstanceInfo(gcp.MetadataClient{}), + log, + ), + } +} diff --git a/internal/attestation/gcp/issuer_test.go b/internal/attestation/gcp/es/issuer_test.go similarity index 86% rename from internal/attestation/gcp/issuer_test.go rename to internal/attestation/gcp/es/issuer_test.go index 4ad64c7a20..4836628553 100644 --- a/internal/attestation/gcp/issuer_test.go +++ b/internal/attestation/gcp/es/issuer_test.go @@ -4,7 +4,7 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ -package gcp +package es import ( "context" @@ -13,6 +13,7 @@ import ( "io" "testing" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" "github.com/google/go-tpm-tools/proto/attest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -66,7 +67,7 @@ func TestGetGCEInstanceInfo(t *testing.T) { require := require.New(t) var tpm io.ReadWriteCloser - out, err := getGCEInstanceInfo(tc.client)(context.Background(), tpm, nil) + out, err := gcp.GCEInstanceInfo(tc.client)(context.Background(), tpm, nil) if tc.wantErr { assert.Error(err) } else { @@ -90,14 +91,14 @@ type fakeMetadataClient struct { zoneErr error } -func (c fakeMetadataClient) projectID() (string, error) { +func (c fakeMetadataClient) ProjectID() (string, error) { return c.projectIDString, c.projecIDErr } -func (c fakeMetadataClient) instanceName() (string, error) { +func (c fakeMetadataClient) InstanceName() (string, error) { return c.instanceNameString, c.instanceNameErr } -func (c fakeMetadataClient) zone() (string, error) { +func (c fakeMetadataClient) Zone() (string, error) { return c.zoneString, c.zoneErr } diff --git a/internal/attestation/gcp/es/validator.go b/internal/attestation/gcp/es/validator.go new file mode 100644 index 0000000000..4177b6f0af --- /dev/null +++ b/internal/attestation/gcp/es/validator.go @@ -0,0 +1,59 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package es + +import ( + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/google/go-tpm-tools/proto/attest" +) + +const minimumGceVersion = 1 + +// Validator for GCP confidential VM attestation. +type Validator struct { + variant.GCPSEVES + *vtpm.Validator +} + +// NewValidator initializes a new GCP validator with the provided PCR values specified in the config. +func NewValidator(cfg *config.GCPSEVES, log attestation.Logger) (*Validator, error) { + getTrustedKey, err := gcp.TrustedKeyGetter(variant.GCPSEVES{}, gcp.NewRESTClient) + if err != nil { + return nil, fmt.Errorf("create trusted key getter: %v", err) + } + + return &Validator{ + Validator: vtpm.NewValidator( + cfg.Measurements, + getTrustedKey, + validateCVM, + log, + ), + }, nil +} + +// validateCVM checks that the machine state represents a GCE AMD-SEV VM. +func validateCVM(_ vtpm.AttestationDocument, state *attest.MachineState) error { + gceVersion := state.Platform.GetGceVersion() + if gceVersion < minimumGceVersion { + return fmt.Errorf("outdated GCE version: %v (require >= %v)", gceVersion, minimumGceVersion) + } + + tech := state.Platform.Technology + wantTech := attest.GCEConfidentialTechnology_AMD_SEV + if tech != wantTech { + return fmt.Errorf("unexpected confidential technology: %v (expected: %v)", tech, wantTech) + } + + return nil +} diff --git a/internal/attestation/gcp/validator_test.go b/internal/attestation/gcp/es/validator_test.go similarity index 92% rename from internal/attestation/gcp/validator_test.go rename to internal/attestation/gcp/es/validator_test.go index 203809a4ff..3fa35da7e5 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 893b002a69..113222dda2 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 4dc36ba0dd..0000000000 --- a/internal/attestation/gcp/issuer.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package gcp - -import ( - "context" - "encoding/json" - "errors" - "io" - - "cloud.google.com/go/compute/metadata" - "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" - tpmclient "github.com/google/go-tpm-tools/client" - "github.com/google/go-tpm-tools/proto/attest" -) - -// Issuer for GCP confidential VM attestation. -type Issuer struct { - variant.GCPSEVES - *vtpm.Issuer -} - -// NewIssuer initializes a new GCP Issuer. -func NewIssuer(log attestation.Logger) *Issuer { - return &Issuer{ - Issuer: vtpm.NewIssuer( - vtpm.OpenVTPM, - tpmclient.GceAttestationKeyRSA, - getGCEInstanceInfo(metadataClient{}), - log, - ), - } -} - -// getGCEInstanceInfo fetches VM metadata used for attestation. -func getGCEInstanceInfo(client gcpMetadataClient) func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) { - // Ideally we would want to use the endorsement public key certificate - // However, this is not available on GCE instances - // Workaround: Provide ShieldedVM instance info - // The attestating party can request the VMs signing key using Google's API - return func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) { - projectID, err := client.projectID() - if err != nil { - return nil, errors.New("unable to fetch projectID") - } - zone, err := client.zone() - if err != nil { - return nil, errors.New("unable to fetch zone") - } - instanceName, err := client.instanceName() - if err != nil { - return nil, errors.New("unable to fetch instance name") - } - - return json.Marshal(&attest.GCEInstanceInfo{ - Zone: zone, - ProjectId: projectID, - InstanceName: instanceName, - }) - } -} - -type gcpMetadataClient interface { - projectID() (string, error) - instanceName() (string, error) - zone() (string, error) -} - -type metadataClient struct{} - -func (c metadataClient) projectID() (string, error) { - return metadata.ProjectID() -} - -func (c metadataClient) instanceName() (string, error) { - return metadata.InstanceName() -} - -func (c metadataClient) zone() (string, error) { - return metadata.Zone() -} diff --git a/internal/attestation/gcp/metadata.go b/internal/attestation/gcp/metadata.go new file mode 100644 index 0000000000..5fdd7046b0 --- /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 0000000000..1a9c277f3e --- /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 0000000000..cef1ff9c81 --- /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 0000000000..59c56e2f95 --- /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 0000000000..ede60f2053 --- /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 0000000000..c178c14ea4 --- /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 310a33b556..0000000000 --- a/internal/attestation/gcp/validator.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package gcp - -import ( - "context" - "crypto" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - - compute "cloud.google.com/go/compute/apiv1" - "cloud.google.com/go/compute/apiv1/computepb" - "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" - "github.com/edgelesssys/constellation/v2/internal/config" - "github.com/google/go-tpm-tools/proto/attest" - "github.com/googleapis/gax-go/v2" - "google.golang.org/api/option" -) - -const minimumGceVersion = 1 - -// Validator for GCP confidential VM attestation. -type Validator struct { - variant.GCPSEVES - *vtpm.Validator - - restClient func(context.Context, ...option.ClientOption) (gcpRestClient, error) -} - -// NewValidator initializes a new GCP validator with the provided PCR values. -func NewValidator(cfg *config.GCPSEVES, log attestation.Logger) *Validator { - v := &Validator{ - restClient: newInstanceClient, - } - v.Validator = vtpm.NewValidator( - cfg.Measurements, - v.trustedKeyFromGCEAPI, - validateCVM, - log, - ) - - return v -} - -type gcpRestClient interface { - GetShieldedInstanceIdentity(ctx context.Context, req *computepb.GetShieldedInstanceIdentityInstanceRequest, opts ...gax.CallOption) (*computepb.ShieldedInstanceIdentity, error) - Close() error -} - -type instanceClient struct { - *compute.InstancesClient -} - -func newInstanceClient(ctx context.Context, opts ...option.ClientOption) (gcpRestClient, error) { - c, err := compute.NewInstancesRESTClient(ctx, opts...) - if err != nil { - return nil, err - } - return &instanceClient{c}, nil -} - -// trustedKeyFromGCEAPI queries the GCE API for a shieldedVM's public signing key. -// This key can be used to verify attestation statements issued by the VM. -func (v *Validator) trustedKeyFromGCEAPI(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { - client, err := v.restClient(ctx) - if err != nil { - return nil, fmt.Errorf("creating GCE client: %w", err) - } - defer client.Close() - - var instanceInfo attest.GCEInstanceInfo - if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil { - return nil, err - } - - instance, err := client.GetShieldedInstanceIdentity(ctx, &computepb.GetShieldedInstanceIdentityInstanceRequest{ - Instance: instanceInfo.GetInstanceName(), - Project: instanceInfo.GetProjectId(), - Zone: instanceInfo.GetZone(), - }) - if err != nil { - return nil, fmt.Errorf("retrieving VM identity: %w", err) - } - - if instance.SigningKey == nil || instance.SigningKey.EkPub == nil { - return nil, fmt.Errorf("received no signing key from GCP API") - } - - // Parse the signing key return by GetShieldedInstanceIdentity - block, _ := pem.Decode([]byte(*instance.SigningKey.EkPub)) - if block == nil || block.Type != "PUBLIC KEY" { - return nil, fmt.Errorf("failed to decode PEM block containing public key") - } - - return x509.ParsePKIXPublicKey(block.Bytes) -} - -// validateCVM checks that the machine state represents a GCE AMD-SEV VM. -func validateCVM(_ vtpm.AttestationDocument, state *attest.MachineState) error { - gceVersion := state.Platform.GetGceVersion() - if gceVersion < minimumGceVersion { - return fmt.Errorf("outdated GCE version: %v (require >= %v)", gceVersion, minimumGceVersion) - } - - tech := state.Platform.Technology - wantTech := attest.GCEConfidentialTechnology_AMD_SEV - if tech != wantTech { - return fmt.Errorf("unexpected confidential technology: %v (expected: %v)", tech, wantTech) - } - - return nil -} diff --git a/internal/attestation/measurements/measurement-generator/generate.go b/internal/attestation/measurements/measurement-generator/generate.go index b552c6b7d2..bdb8e943f6 100644 --- a/internal/attestation/measurements/measurement-generator/generate.go +++ b/internal/attestation/measurements/measurement-generator/generate.go @@ -84,9 +84,9 @@ func main() { log.Println("Found", variant) returnStmtCtr++ // retrieve and validate measurements for the given CSP and image - measuremnts := mustGetMeasurements(ctx, rekor, provider, variant, defaultImage) + measurements := mustGetMeasurements(ctx, rekor, provider, variant, defaultImage) // replace the return statement with a composite literal containing the validated measurements - clause.Values[0] = measurementsCompositeLiteral(measuremnts) + clause.Values[0] = measurementsCompositeLiteral(measurements) } return true }, nil, @@ -267,6 +267,8 @@ func attestationVariantFromGoIdentifier(identifier string) (variant.Variant, err return variant.AWSNitroTPM{}, nil case "GCPSEVES": return variant.GCPSEVES{}, nil + case "GCPSEVSNP": + return variant.GCPSEVSNP{}, nil case "AzureSEVSNP": return variant.AzureSEVSNP{}, nil case "AzureTDX": diff --git a/internal/attestation/measurements/measurements.go b/internal/attestation/measurements/measurements.go index a702706bd3..1bea6174d0 100644 --- a/internal/attestation/measurements/measurements.go +++ b/internal/attestation/measurements/measurements.go @@ -516,6 +516,9 @@ func DefaultsFor(provider cloudprovider.Provider, attestationVariant variant.Var case provider == cloudprovider.GCP && attestationVariant == variant.GCPSEVES{}: return gcp_GCPSEVES.Copy() + case provider == cloudprovider.GCP && attestationVariant == variant.GCPSEVSNP{}: + return gcp_GCPSEVSNP.Copy() + case provider == cloudprovider.OpenStack && attestationVariant == variant.QEMUVTPM{}: return openstack_QEMUVTPM.Copy() diff --git a/internal/attestation/measurements/measurements_enterprise.go b/internal/attestation/measurements/measurements_enterprise.go index a3d09b0493..b8e6990127 100644 --- a/internal/attestation/measurements/measurements_enterprise.go +++ b/internal/attestation/measurements/measurements_enterprise.go @@ -22,6 +22,7 @@ var ( azure_AzureTDX = M{1: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0xb9, 0x41, 0x89, 0x67, 0xb5, 0x5d, 0x99, 0x24, 0xc8, 0x2c, 0xc3, 0x6d, 0xe8, 0x09, 0xac, 0xa7, 0xeb, 0x7b, 0x01, 0xf1, 0x94, 0x03, 0x84, 0xde, 0x25, 0x89, 0xe1, 0x37, 0xb4, 0x51, 0xb4, 0x8e}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x1e, 0xc8, 0x89, 0x5b, 0x93, 0x81, 0xe7, 0x06, 0xc6, 0x7d, 0x8d, 0x30, 0xf1, 0x95, 0x53, 0x64, 0xd7, 0x41, 0x9a, 0x9e, 0x85, 0x04, 0x9f, 0x7e, 0x19, 0xf1, 0x7e, 0x05, 0x1c, 0xc5, 0xe0, 0x4c}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x30, 0x3e, 0x47, 0xd3, 0x52, 0x90, 0x0d, 0x55, 0xdb, 0xad, 0xe3, 0x2a, 0x41, 0x1e, 0xeb, 0xd9, 0x28, 0x59, 0x87, 0xf2, 0x56, 0xcf, 0xdd, 0x60, 0x8a, 0xe2, 0x1a, 0xce, 0xbf, 0x2e, 0xda, 0x76}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} azure_AzureTrustedLaunch M gcp_GCPSEVES = M{1: {Expected: []byte{0x36, 0x95, 0xdc, 0xc5, 0x5e, 0x3a, 0xa3, 0x40, 0x27, 0xc2, 0x77, 0x93, 0xc8, 0x5c, 0x72, 0x3c, 0x69, 0x7d, 0x70, 0x8c, 0x42, 0xd1, 0xf7, 0x3b, 0xd6, 0xfa, 0x4f, 0x26, 0x60, 0x8a, 0x5b, 0x24}, ValidationOpt: WarnOnly}, 2: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 3: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 4: {Expected: []byte{0xce, 0x7d, 0x34, 0x06, 0xe1, 0xde, 0xb3, 0x35, 0x21, 0x98, 0x95, 0xee, 0x33, 0x16, 0xd2, 0x63, 0xf3, 0x20, 0x1f, 0x32, 0xc9, 0x70, 0xde, 0x8c, 0x24, 0x87, 0x65, 0x92, 0xf4, 0x72, 0x11, 0x5d}, ValidationOpt: Enforce}, 6: {Expected: []byte{0x3d, 0x45, 0x8c, 0xfe, 0x55, 0xcc, 0x03, 0xea, 0x1f, 0x44, 0x3f, 0x15, 0x62, 0xbe, 0xec, 0x8d, 0xf5, 0x1c, 0x75, 0xe1, 0x4a, 0x9f, 0xcf, 0x9a, 0x72, 0x34, 0xa1, 0x3f, 0x19, 0x8e, 0x79, 0x69}, ValidationOpt: WarnOnly}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x82, 0xb1, 0x4e, 0x09, 0xf0, 0xaf, 0x8a, 0x38, 0xc5, 0x4e, 0x44, 0x4f, 0xe7, 0x5e, 0x1d, 0xbe, 0xca, 0xd2, 0x88, 0xd0, 0x15, 0xd9, 0xef, 0x37, 0x11, 0x75, 0x0a, 0x78, 0x25, 0xad, 0x32, 0x4a}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x38, 0x51, 0xe5, 0xc2, 0x29, 0x86, 0x01, 0xa5, 0x0f, 0xea, 0xd3, 0xeb, 0x46, 0x86, 0xc7, 0x75, 0xae, 0x26, 0xe6, 0x02, 0x7c, 0x4f, 0xdc, 0xc2, 0xfe, 0xd2, 0x9e, 0x8c, 0xc4, 0x55, 0x45, 0x62}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} + gcp_GCPSEVSNP M openstack_QEMUVTPM = M{4: {Expected: []byte{0xba, 0x9a, 0x57, 0xc4, 0xa6, 0xee, 0xc4, 0x0c, 0xe4, 0x78, 0x09, 0x39, 0x7a, 0xb2, 0xa2, 0x71, 0x71, 0x62, 0xcb, 0xd7, 0x75, 0xd9, 0x32, 0x3c, 0xc6, 0x11, 0x77, 0xab, 0xc1, 0x95, 0x34, 0x9b}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0x3f, 0x40, 0x48, 0xb6, 0xea, 0x59, 0xe6, 0x80, 0xf6, 0xc8, 0xb0, 0xbe, 0x9b, 0xd3, 0x45, 0x8a, 0x2d, 0x96, 0x99, 0x8d, 0x6b, 0x6a, 0xff, 0xcc, 0x0c, 0xa7, 0x27, 0x1b, 0x04, 0xb8, 0x6f, 0x58}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x9c, 0x9f, 0x5e, 0xf4, 0x18, 0xa8, 0xe9, 0x40, 0x08, 0xf7, 0xd7, 0x89, 0x65, 0x3c, 0x04, 0xd0, 0x1f, 0xc0, 0xaa, 0x07, 0xf5, 0xb3, 0x7a, 0xa3, 0x27, 0x36, 0x1a, 0x0c, 0x65, 0x17, 0x29, 0xdb}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 14: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: WarnOnly}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} qemu_QEMUTDX M qemu_QEMUVTPM = M{4: {Expected: []byte{0xb5, 0x42, 0x65, 0x31, 0x43, 0x95, 0x1d, 0x45, 0x1a, 0x8d, 0x75, 0x99, 0xef, 0x71, 0x1f, 0xdd, 0xe3, 0xb6, 0x9c, 0x14, 0x3a, 0x2b, 0x43, 0x04, 0x12, 0x1d, 0x32, 0x85, 0xde, 0xeb, 0xff, 0xd8}, ValidationOpt: Enforce}, 8: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 9: {Expected: []byte{0xc3, 0xdc, 0x39, 0x88, 0x41, 0x3b, 0x41, 0x95, 0xed, 0x68, 0x5d, 0x99, 0x56, 0x0a, 0x0c, 0xa8, 0x20, 0x43, 0x0e, 0x66, 0xc2, 0x34, 0xa7, 0x55, 0x6f, 0x49, 0xb3, 0x68, 0xf5, 0x76, 0x39, 0xca}, ValidationOpt: Enforce}, 11: {Expected: []byte{0x64, 0x54, 0x4f, 0xe0, 0x2f, 0x51, 0x78, 0x7f, 0x06, 0x74, 0x26, 0xd5, 0xdc, 0xb7, 0x91, 0x72, 0x94, 0x0b, 0x52, 0x13, 0x17, 0x8c, 0x08, 0x38, 0xf6, 0x17, 0x83, 0x54, 0x22, 0x9a, 0x49, 0x9d}, ValidationOpt: Enforce}, 12: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 13: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}, 15: {Expected: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ValidationOpt: Enforce}} diff --git a/internal/attestation/measurements/measurements_oss.go b/internal/attestation/measurements/measurements_oss.go index 552d6bd261..0ef7ce640c 100644 --- a/internal/attestation/measurements/measurements_oss.go +++ b/internal/attestation/measurements/measurements_oss.go @@ -64,6 +64,15 @@ var ( 13: WithAllBytes(0x00, Enforce, PCRMeasurementLength), uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength), } + gcp_GCPSEVSNP = M{ + 4: PlaceHolderMeasurement(PCRMeasurementLength), + 8: WithAllBytes(0x00, Enforce, PCRMeasurementLength), + 9: PlaceHolderMeasurement(PCRMeasurementLength), + 11: WithAllBytes(0x00, Enforce, PCRMeasurementLength), + 12: PlaceHolderMeasurement(PCRMeasurementLength), + 13: WithAllBytes(0x00, Enforce, PCRMeasurementLength), + uint32(PCRIndexClusterID): WithAllBytes(0x00, Enforce, PCRMeasurementLength), + } openstack_QEMUVTPM = M{ 4: PlaceHolderMeasurement(PCRMeasurementLength), 8: WithAllBytes(0x00, Enforce, PCRMeasurementLength), diff --git a/internal/attestation/snp/BUILD.bazel b/internal/attestation/snp/BUILD.bazel index 700a3aa865..f62518f254 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 95cba55bfc..c341e31fa9 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 0179ac05b4..2eaf3e52a0 100644 --- a/internal/attestation/snp/snp_test.go +++ b/internal/attestation/snp/snp_test.go @@ -149,12 +149,24 @@ func TestAttestationWithCerts(t *testing.T) { wantErr bool }{ "success": { + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + reportSigner: testdata.AzureThimVCEK, + certChain: testdata.CertChain, + fallbackCerts: CertificateChain{ark: testdataArk}, + expectedArk: testdataArk, + expectedAsk: testdataAsk, + getter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), + }, + "ark only in pre-fetched cert-chain": { report: defaultReport, idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", reportSigner: testdata.AzureThimVCEK, certChain: testdata.CertChain, expectedArk: testdataArk, expectedAsk: testdataAsk, + getter: newStubHTTPSGetter(nil, assert.AnError), + wantErr: true, }, "vlek success": { report: vlekReport, @@ -173,9 +185,10 @@ func TestAttestationWithCerts(t *testing.T) { ), }, "retrieve vcek": { - report: defaultReport, - idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", - certChain: testdata.CertChain, + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + certChain: testdata.CertChain, + fallbackCerts: CertificateChain{ark: testdataArk}, getter: newStubHTTPSGetter( &urlResponseMatcher{ vcekResponse: testdata.AmdKdsVCEK, @@ -205,25 +218,9 @@ func TestAttestationWithCerts(t *testing.T) { idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", reportSigner: testdata.AzureThimVCEK, fallbackCerts: NewCertificateChain(exampleCert, exampleCert), - getter: newStubHTTPSGetter( - &urlResponseMatcher{}, - nil, - ), - expectedArk: exampleCert, - expectedAsk: exampleCert, - }, - "use certchain with fallback certs": { - report: defaultReport, - idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", - certChain: testdata.CertChain, - reportSigner: testdata.AzureThimVCEK, - fallbackCerts: NewCertificateChain(&x509.Certificate{}, &x509.Certificate{}), - getter: newStubHTTPSGetter( - &urlResponseMatcher{}, - nil, - ), - expectedArk: testdataArk, - expectedAsk: testdataAsk, + getter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), + expectedArk: exampleCert, + expectedAsk: exampleCert, }, "retrieve vcek and certchain": { report: defaultReport, @@ -242,10 +239,12 @@ func TestAttestationWithCerts(t *testing.T) { }, "report too short": { report: defaultReport[:len(defaultReport)-100], + getter: newStubHTTPSGetter(nil, assert.AnError), wantErr: true, }, "corrupted report": { report: defaultReport[10 : len(defaultReport)-10], + getter: newStubHTTPSGetter(nil, assert.AnError), wantErr: true, }, "certificate fetch error": { diff --git a/internal/attestation/variant/variant.go b/internal/attestation/variant/variant.go index 43397a94b5..e71a51480d 100644 --- a/internal/attestation/variant/variant.go +++ b/internal/attestation/variant/variant.go @@ -44,6 +44,7 @@ const ( awsNitroTPM = "aws-nitro-tpm" awsSEVSNP = "aws-sev-snp" gcpSEVES = "gcp-sev-es" + gcpSEVSNP = "gcp-sev-snp" azureTDX = "azure-tdx" azureSEVSNP = "azure-sev-snp" azureTrustedLaunch = "azure-trustedlaunch" @@ -54,7 +55,7 @@ const ( var providerAttestationMapping = map[cloudprovider.Provider][]Variant{ cloudprovider.AWS: {AWSSEVSNP{}, AWSNitroTPM{}}, cloudprovider.Azure: {AzureSEVSNP{}, AzureTDX{}, AzureTrustedLaunch{}}, - cloudprovider.GCP: {GCPSEVES{}}, + cloudprovider.GCP: {GCPSEVES{}, GCPSEVSNP{}}, cloudprovider.QEMU: {QEMUVTPM{}}, cloudprovider.OpenStack: {QEMUVTPM{}}, } @@ -110,6 +111,8 @@ func FromString(oid string) (Variant, error) { return AWSNitroTPM{}, nil case gcpSEVES: return GCPSEVES{}, nil + case gcpSEVSNP: + return GCPSEVSNP{}, nil case azureSEVSNP: return AzureSEVSNP{}, nil case azureTrustedLaunch: @@ -209,6 +212,24 @@ func (GCPSEVES) Equal(other Getter) bool { return other.OID().Equal(GCPSEVES{}.OID()) } +// GCPSEVSNP holds the GCP SEV-SNP OID. +type GCPSEVSNP struct{} + +// OID returns the struct's object identifier. +func (GCPSEVSNP) OID() asn1.ObjectIdentifier { + return asn1.ObjectIdentifier{1, 3, 9900, 3, 2} +} + +// String returns the string representation of the OID. +func (GCPSEVSNP) String() string { + return gcpSEVSNP +} + +// Equal returns true if the other variant is also GCPSEVSNP. +func (GCPSEVSNP) Equal(other Getter) bool { + return other.OID().Equal(GCPSEVSNP{}.OID()) +} + // AzureTDX holds the OID for Azure TDX CVMs. type AzureTDX struct{} diff --git a/internal/attestation/vtpm/attestation.go b/internal/attestation/vtpm/attestation.go index 77c396b9a4..364ab11632 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 c653c489c3..8ea071ae89 100644 --- a/internal/config/BUILD.bazel +++ b/internal/config/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "azure.go", "config.go", "config_doc.go", + "gcp.go", # keep "image_enterprise.go", # keep diff --git a/internal/config/attestation.go b/internal/config/attestation.go index dc4d8fb83c..f635ebbbdc 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -52,6 +52,8 @@ func UnmarshalAttestationConfig(data []byte, attestVariant variant.Variant) (Att return unmarshalTypedConfig[*AzureTDX](data) case variant.GCPSEVES{}: return unmarshalTypedConfig[*GCPSEVES](data) + case variant.GCPSEVSNP{}: + return unmarshalTypedConfig[*GCPSEVSNP](data) case variant.QEMUVTPM{}: return unmarshalTypedConfig[*QEMUVTPM](data) case variant.QEMUTDX{}: diff --git a/internal/config/attestation_test.go b/internal/config/attestation_test.go index e0e3492dce..a690ba40b9 100644 --- a/internal/config/attestation_test.go +++ b/internal/config/attestation_test.go @@ -41,6 +41,9 @@ func TestUnmarshalAttestationConfig(t *testing.T) { "GCPSEVES": { cfg: &GCPSEVES{Measurements: measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVES{})}, }, + "GCPSEVSNP": { + cfg: DefaultForGCPSEVSNP(), + }, "QEMUVTPM": { cfg: &QEMUVTPM{Measurements: measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUVTPM{})}, }, diff --git a/internal/config/config.go b/internal/config/config.go index 10ac013d14..c3ea6b34de 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 2168b7f988..56f358d03e 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -23,6 +23,7 @@ var ( UnsupportedAppRegistrationErrorDoc encoder.Doc SNPFirmwareSignerConfigDoc encoder.Doc GCPSEVESDoc encoder.Doc + GCPSEVSNPDoc encoder.Doc QEMUVTPMDoc encoder.Doc QEMUTDXDoc encoder.Doc AWSSEVSNPDoc encoder.Doc @@ -388,7 +389,7 @@ func init() { FieldName: "attestation", }, } - AttestationConfigDoc.Fields = make([]encoder.Doc, 8) + AttestationConfigDoc.Fields = make([]encoder.Doc, 9) AttestationConfigDoc.Fields[0].Name = "awsSEVSNP" AttestationConfigDoc.Fields[0].Type = "AWSSEVSNP" AttestationConfigDoc.Fields[0].Note = "" @@ -419,16 +420,21 @@ func init() { AttestationConfigDoc.Fields[5].Note = "" AttestationConfigDoc.Fields[5].Description = "GCP SEV-ES attestation." AttestationConfigDoc.Fields[5].Comments[encoder.LineComment] = "GCP SEV-ES attestation." - AttestationConfigDoc.Fields[6].Name = "qemuTDX" - AttestationConfigDoc.Fields[6].Type = "QEMUTDX" + AttestationConfigDoc.Fields[6].Name = "gcpSEVSNP" + AttestationConfigDoc.Fields[6].Type = "GCPSEVSNP" AttestationConfigDoc.Fields[6].Note = "" - AttestationConfigDoc.Fields[6].Description = "QEMU tdx attestation." - AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "QEMU tdx attestation." - AttestationConfigDoc.Fields[7].Name = "qemuVTPM" - AttestationConfigDoc.Fields[7].Type = "QEMUVTPM" + AttestationConfigDoc.Fields[6].Description = "description: |\n GCP SEV-SNP attestation.\n" + AttestationConfigDoc.Fields[6].Comments[encoder.LineComment] = "description: |" + AttestationConfigDoc.Fields[7].Name = "qemuTDX" + AttestationConfigDoc.Fields[7].Type = "QEMUTDX" AttestationConfigDoc.Fields[7].Note = "" - AttestationConfigDoc.Fields[7].Description = "QEMU vTPM attestation." - AttestationConfigDoc.Fields[7].Comments[encoder.LineComment] = "QEMU vTPM attestation." + AttestationConfigDoc.Fields[7].Description = "QEMU tdx attestation." + AttestationConfigDoc.Fields[7].Comments[encoder.LineComment] = "QEMU tdx attestation." + AttestationConfigDoc.Fields[8].Name = "qemuVTPM" + AttestationConfigDoc.Fields[8].Type = "QEMUVTPM" + AttestationConfigDoc.Fields[8].Note = "" + AttestationConfigDoc.Fields[8].Description = "QEMU vTPM attestation." + AttestationConfigDoc.Fields[8].Comments[encoder.LineComment] = "QEMU vTPM attestation." NodeGroupDoc.Type = "NodeGroup" NodeGroupDoc.Comments[encoder.LineComment] = "NodeGroup defines a group of nodes with the same role and configuration." @@ -518,6 +524,52 @@ func init() { GCPSEVESDoc.Fields[0].Description = "Expected TPM measurements." GCPSEVESDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + GCPSEVSNPDoc.Type = "GCPSEVSNP" + GCPSEVSNPDoc.Comments[encoder.LineComment] = "GCPSEVSNP is the configuration for GCP SEV-SNP attestation." + GCPSEVSNPDoc.Description = "GCPSEVSNP is the configuration for GCP SEV-SNP attestation." + GCPSEVSNPDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "AttestationConfig", + FieldName: "gcpSEVSNP", + }, + } + GCPSEVSNPDoc.Fields = make([]encoder.Doc, 7) + GCPSEVSNPDoc.Fields[0].Name = "measurements" + GCPSEVSNPDoc.Fields[0].Type = "M" + GCPSEVSNPDoc.Fields[0].Note = "" + GCPSEVSNPDoc.Fields[0].Description = "Expected TPM measurements." + GCPSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + GCPSEVSNPDoc.Fields[1].Name = "bootloaderVersion" + GCPSEVSNPDoc.Fields[1].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[1].Note = "" + GCPSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version." + GCPSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version." + GCPSEVSNPDoc.Fields[2].Name = "teeVersion" + GCPSEVSNPDoc.Fields[2].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[2].Note = "" + GCPSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version." + GCPSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version." + GCPSEVSNPDoc.Fields[3].Name = "snpVersion" + GCPSEVSNPDoc.Fields[3].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[3].Note = "" + GCPSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version." + GCPSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version." + GCPSEVSNPDoc.Fields[4].Name = "microcodeVersion" + GCPSEVSNPDoc.Fields[4].Type = "AttestationVersion" + GCPSEVSNPDoc.Fields[4].Note = "" + GCPSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version." + GCPSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version." + GCPSEVSNPDoc.Fields[5].Name = "amdRootKey" + GCPSEVSNPDoc.Fields[5].Type = "Certificate" + GCPSEVSNPDoc.Fields[5].Note = "" + GCPSEVSNPDoc.Fields[5].Description = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + GCPSEVSNPDoc.Fields[5].Comments[encoder.LineComment] = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + GCPSEVSNPDoc.Fields[6].Name = "amdSigningKey" + GCPSEVSNPDoc.Fields[6].Type = "Certificate" + GCPSEVSNPDoc.Fields[6].Note = "" + GCPSEVSNPDoc.Fields[6].Description = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + GCPSEVSNPDoc.Fields[6].Comments[encoder.LineComment] = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + QEMUVTPMDoc.Type = "QEMUVTPM" QEMUVTPMDoc.Comments[encoder.LineComment] = "QEMUVTPM is the configuration for QEMU vTPM attestation." QEMUVTPMDoc.Description = "QEMUVTPM is the configuration for QEMU vTPM attestation." @@ -779,6 +831,10 @@ func (_ GCPSEVES) Doc() *encoder.Doc { return &GCPSEVESDoc } +func (_ GCPSEVSNP) Doc() *encoder.Doc { + return &GCPSEVSNPDoc +} + func (_ QEMUVTPM) Doc() *encoder.Doc { return &QEMUVTPMDoc } @@ -825,6 +881,7 @@ func GetConfigurationDoc() *encoder.FileDoc { &UnsupportedAppRegistrationErrorDoc, &SNPFirmwareSignerConfigDoc, &GCPSEVESDoc, + &GCPSEVSNPDoc, &QEMUVTPMDoc, &QEMUTDXDoc, &AWSSEVSNPDoc, diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 013c50edcf..fa9c0c2d0e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -328,7 +328,7 @@ func TestFromFile(t *testing.T) { } func TestValidate(t *testing.T) { - const defaultErrCount = 32 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default + const defaultErrCount = 33 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default const azErrCount = 7 const awsErrCount = 8 const gcpErrCount = 8 @@ -735,6 +735,11 @@ func TestValidInstanceTypeForProvider(t *testing.T) { instanceTypes: instancetypes.GCPInstanceTypes, expectedResult: true, }, + "gcp sev-snp": { + variant: variant.GCPSEVSNP{}, + instanceTypes: instancetypes.GCPInstanceTypes, + expectedResult: true, + }, "put gcp when azure is set": { variant: variant.AzureSEVSNP{}, instanceTypes: instancetypes.GCPInstanceTypes, diff --git a/internal/config/gcp.go b/internal/config/gcp.go new file mode 100644 index 0000000000..847474f05b --- /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 5e0ef59ee0..fab69ff29e 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -202,6 +202,9 @@ func validateAttestation(sl validator.StructLevel) { if attestation.GCPSEVES != nil { attestationCount++ } + if attestation.GCPSEVSNP != nil { + attestationCount++ + } if attestation.QEMUVTPM != nil { attestationCount++ } @@ -364,6 +367,9 @@ func (c *Config) translateMoreThanOneAttestationError(ut ut.Translator, fe valid if c.Attestation.GCPSEVES != nil { definedAttestations = append(definedAttestations, "GCPSEVES") } + if c.Attestation.GCPSEVSNP != nil { + definedAttestations = append(definedAttestations, "GCPSEVSNP") + } if c.Attestation.QEMUVTPM != nil { definedAttestations = append(definedAttestations, "QEMUVTPM") } @@ -536,7 +542,7 @@ func validInstanceTypeForProvider(insType string, attestation variant.Variant) b return true } } - case variant.GCPSEVES{}: + case variant.GCPSEVES{}, variant.GCPSEVSNP{}: for _, instanceType := range instancetypes.GCPInstanceTypes { if insType == instanceType { return true diff --git a/internal/constellation/state/state.go b/internal/constellation/state/state.go index bee5f8b2b7..68e9b2845b 100644 --- a/internal/constellation/state/state.go +++ b/internal/constellation/state/state.go @@ -383,7 +383,7 @@ func (s *State) preInitConstraints(attestation variant.Variant) func() []*valida ), ) } - case variant.GCPSEVES{}: + case variant.GCPSEVES{}, variant.GCPSEVSNP{}: // GCP values need to be valid after infrastructure creation. constraints = append(constraints, // Azure values need to be nil or empty. @@ -514,7 +514,7 @@ func (s *State) postInitConstraints(attestation variant.Variant) func() []*valid ), ) } - case variant.GCPSEVES{}: + case variant.GCPSEVES{}, variant.GCPSEVSNP{}: constraints = append(constraints, // Azure values need to be nil or empty. validation.Or( diff --git a/measurement-reader/cmd/main.go b/measurement-reader/cmd/main.go index 15ce68d2ea..9bdc44332c 100644 --- a/measurement-reader/cmd/main.go +++ b/measurement-reader/cmd/main.go @@ -30,7 +30,7 @@ func main() { var m []sorted.Measurement switch attestationVariant { - case variant.AWSNitroTPM{}, variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.AzureTrustedLaunch{}, variant.GCPSEVES{}, variant.QEMUVTPM{}: + case variant.AWSNitroTPM{}, variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.AzureTrustedLaunch{}, variant.GCPSEVES{}, variant.GCPSEVSNP{}, variant.QEMUVTPM{}: m, err = tpm.Measurements() if err != nil { log.With(slog.Any("error", err)).Error("Failed to read TPM measurements") diff --git a/terraform-provider-constellation/docs/data-sources/attestation.md b/terraform-provider-constellation/docs/data-sources/attestation.md index ec4118c0f7..b1b8891c01 100644 --- a/terraform-provider-constellation/docs/data-sources/attestation.md +++ b/terraform-provider-constellation/docs/data-sources/attestation.md @@ -33,6 +33,7 @@ data "constellation_attestation" "test" { * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` - `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`) See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports. @@ -83,6 +84,7 @@ Read-Only: * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` diff --git a/terraform-provider-constellation/docs/data-sources/image.md b/terraform-provider-constellation/docs/data-sources/image.md index 7f7186b56f..f0b37455a2 100644 --- a/terraform-provider-constellation/docs/data-sources/image.md +++ b/terraform-provider-constellation/docs/data-sources/image.md @@ -32,6 +32,7 @@ data "constellation_image" "example" { * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` - `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`) See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports. diff --git a/terraform-provider-constellation/docs/resources/cluster.md b/terraform-provider-constellation/docs/resources/cluster.md index 7b6d1ca210..cf77d1f746 100644 --- a/terraform-provider-constellation/docs/resources/cluster.md +++ b/terraform-provider-constellation/docs/resources/cluster.md @@ -111,6 +111,7 @@ Required: * `azure-sev-snp` * `azure-tdx` * `gcp-sev-es` + * `gcp-sev-snp` * `qemu-vtpm` Optional: diff --git a/terraform-provider-constellation/examples/full/gcp/main.tf b/terraform-provider-constellation/examples/full/gcp/main.tf index f7ac80b04e..1cbbd525c0 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 56815ae223..abf2921b69 100644 --- a/terraform-provider-constellation/internal/provider/attestation_data_source.go +++ b/terraform-provider-constellation/internal/provider/attestation_data_source.go @@ -163,7 +163,9 @@ func (d *AttestationDataSource) Read(ctx context.Context, req datasource.ReadReq insecureFetch := data.Insecure.ValueBool() snpVersions := attestationconfigapi.SEVSNPVersionAPI{} - if attestationVariant.Equal(variant.AzureSEVSNP{}) || attestationVariant.Equal(variant.AWSSEVSNP{}) { + if attestationVariant.Equal(variant.AzureSEVSNP{}) || + attestationVariant.Equal(variant.AWSSEVSNP{}) || + attestationVariant.Equal(variant.GCPSEVSNP{}) { snpVersions, err = d.fetcher.FetchSEVSNPVersionLatest(ctx, attestationVariant) if err != nil { resp.Diagnostics.AddError("Fetching SNP Version numbers", err.Error()) diff --git a/terraform-provider-constellation/internal/provider/attestation_data_source_test.go b/terraform-provider-constellation/internal/provider/attestation_data_source_test.go index 4fed9fbe32..3740e6b114 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 0877281683..cfe9ec7fab 100644 --- a/terraform-provider-constellation/internal/provider/convert.go +++ b/terraform-provider-constellation/internal/provider/convert.go @@ -122,6 +122,10 @@ func convertFromTfAttestationCfg(tfAttestation attestationAttribute, attestation attestationConfig = &config.GCPSEVES{ Measurements: c11nMeasurements, } + case variant.GCPSEVSNP{}: + attestationConfig = &config.GCPSEVSNP{ + Measurements: c11nMeasurements, + } case variant.QEMUVTPM{}: attestationConfig = &config.QEMUVTPM{ Measurements: c11nMeasurements, @@ -150,6 +154,13 @@ func convertToTfAttestation(attVar variant.Variant, snpVersions attestationconfi } tfAttestation.AMDRootKey = certStr + case variant.GCPSEVSNP{}: + certStr, err := certAsString(config.DefaultForGCPSEVSNP().AMDRootKey) + if err != nil { + return tfAttestation, err + } + tfAttestation.AMDRootKey = certStr + case variant.AzureSEVSNP{}: certStr, err := certAsString(config.DefaultForAzureSEVSNP().AMDRootKey) if err != nil { diff --git a/terraform-provider-constellation/internal/provider/image_data_source_test.go b/terraform-provider-constellation/internal/provider/image_data_source_test.go index 669899e396..986ee1b534 100644 --- a/terraform-provider-constellation/internal/provider/image_data_source_test.go +++ b/terraform-provider-constellation/internal/provider/image_data_source_test.go @@ -125,7 +125,7 @@ func TestAccImageDataSource(t *testing.T) { }, }, }, - "gcp success": { + "gcp sev-es success": { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, PreCheck: bazelPreCheck, Steps: []resource.TestStep{ @@ -141,6 +141,23 @@ func TestAccImageDataSource(t *testing.T) { }, }, }, + // TODO(msanft): Enable once v2.17.0 is available + // "gcp sev-snp success": { + // ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + // PreCheck: bazelPreCheck, + // Steps: []resource.TestStep{ + // { + // Config: testingConfig + ` + // data "constellation_image" "test" { + // version = "v2.17.0" + // attestation_variant = "gcp-sev-snp" + // csp = "gcp" + // } + // `, + // Check: resource.TestCheckResourceAttr("data.constellation_image.test", "image.reference", "projects/constellation-images/global/images/v2-13-0-gcp-sev-es-stable"), // should be immutable, + // }, + // }, + // }, "stackit success": { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, PreCheck: bazelPreCheck, diff --git a/terraform-provider-constellation/internal/provider/shared_attributes.go b/terraform-provider-constellation/internal/provider/shared_attributes.go index b6f96cd170..0a8c6a72d3 100644 --- a/terraform-provider-constellation/internal/provider/shared_attributes.go +++ b/terraform-provider-constellation/internal/provider/shared_attributes.go @@ -32,11 +32,12 @@ func newAttestationVariantAttributeSchema(t attributeType) schema.Attribute { " * `azure-sev-snp`\n" + " * `azure-tdx`\n" + " * `gcp-sev-es`\n" + + " * `gcp-sev-snp`\n" + " * `qemu-vtpm`\n", Required: isInput, Computed: !isInput, Validators: []validator.String{ - stringvalidator.OneOf("aws-sev-snp", "aws-nitro-tpm", "azure-sev-snp", "azure-tdx", "gcp-sev-es", "qemu-vtpm"), + stringvalidator.OneOf("aws-sev-snp", "aws-nitro-tpm", "azure-sev-snp", "azure-tdx", "gcp-sev-es", "gcp-sev-snp", "qemu-vtpm"), }, } } diff --git a/terraform/infrastructure/gcp/.terraform.lock.hcl b/terraform/infrastructure/gcp/.terraform.lock.hcl index bc58c4246c..5579933811 100644 --- a/terraform/infrastructure/gcp/.terraform.lock.hcl +++ b/terraform/infrastructure/gcp/.terraform.lock.hcl @@ -2,26 +2,50 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { - version = "5.17.0" - constraints = "5.17.0" + version = "5.23.0" + constraints = "5.23.0" hashes = [ - "h1:9DKCaGp9EFKDLWIOWI3yA/RgWTMh0EMD6+iggVXC9l0=", - "h1:JEfDiodirnMqwNaub/anXoOtWt68aEN80QtPJxg3jsc=", - "h1:TANQI64JuScQ2LTITQqz7eh1RjhYDItdbI5p1aBOtXY=", - "h1:dT3UftIyARC7YjS4yurPlNS7WJAHICDHMXSluAAvavA=", - "h1:lu84RYioCT4OxXbFBdqom4QvSPAjMkEyHPSIAxuS7oo=", - "zh:31b4d485ee66e6ff2eb1d8e476e694904447ce2b7143a2e067e4b80a84958d13", - "zh:32e86a51c4b0b29b7a18dd95616ea2976f08a4a7385e00f2bcab266217ee4320", - "zh:357f352bf04e7bc10d61d49296bf6503f31a3db0500169cb532afde7d318643e", - "zh:4b4637ca397cc771136edf7ec5578b5ab8631a8955a86d4fce3b8c40ca8c26b4", - "zh:4fe198b7427f7bf04270a5491a0352379c2b0a1caf12e206e6e224ceb085f56a", - "zh:7abb8509a61602d5ed4c801e7cd7c8299d109bc07980352251ba79880a99abab", - "zh:b1550fe08c650d8419860da1568d3f77093d269f880cad7d720d843b2a9ec545", - "zh:c91d7079646a3fdbb927085e368a16b221a23c17cf7455d5088f0c8f5da48c9f", - "zh:d367213a5f392852ef0708283df583703b2efd0b44f9e599cd055086c371cf74", - "zh:d5b557f294f4094a865afaa0611dc2e657d485b60903f12795eeedc2e1c3aa87", + "h1:2VJTKCZWQ1DaNwclFxSo27avsYwWgq/itwLZ3xKyl/o=", + "h1:4evtipODvV5s86gihS+jyk1cSW1xLn22jy8Ox8zzhAs=", + "h1:BD+iQfFcZ0OeaZI2JWDp2sLqSr+DfZtWy4yo1OVMnTI=", + "h1:my3kqg4hIpWLu2WwRewOFxBS+FXfkAIiw8xTYVPNS9M=", + "h1:xpm8QPNp2soGqIEnf4SNoZaTlQ/SbNH63BooJkSbgX0=", + "zh:18eaaa51a8b30fed61c73799b8716a9bd08ccd382bc395c63e45b9a52ed8b300", + "zh:20c71acf091a282db88473ec6f0a684ac59891713c49b2ff1cb35c1539da3121", + "zh:2e3e9ae1d3b045dcaa39053f4d1d066fa17e5b81f4ed7a5e57cc4e6e1e651900", + "zh:531d1552f251c5a0176543defa95c2cc259fc8b9359ef6fd3df404dcead555a0", + "zh:67a7800023fa09a7d87ac02231364988749663e37e2906aa89c70eecc5955ccf", + "zh:6a8076b59d2766a05ffe521cc115f3e8df7cd2ee4c6d60de4ee4636f47714f2e", + "zh:7b39fe720bb7a1f35cd0e4dfeff617338342fc2d16bb22274b42c080ff633140", + "zh:b181e04c32aa53ad78eaf6f2746ec5fd94977187ba7314ae8e9815ef6ea56532", + "zh:bf605be2f8942d5cabb8755ff0d18f243b53f1148f5f32db762667cf64bfa949", + "zh:e981988558310df5d94e56adaa76f7444d991357fe9600c46eb70fa61f4a1394", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f663776d79e7e5d131b4fbd68c152f2bef3e899a19c9baabe3a441e3f5e809ea", + ] +} + +provider "registry.terraform.io/hashicorp/google-beta" { + version = "5.23.0" + constraints = "5.23.0" + hashes = [ + "h1:EGIz78npj995XQdJyKRqgiCqFrcfDPXJwVrVw3PFGE0=", + "h1:cxF5B8zWRmTStRAY5o+A3iIFtsiKN0NNr72YTtKSSJw=", + "h1:irFKUONsaAiMFJPCyViRAuIWH/aRUKjEzL5mwzSMMRY=", + "h1:kiwwYe7qrzmxT5L/T6kuWMSqSR5THlGybmZ17hxpPI4=", + "h1:lvEvKrY8nPjumNwHxRmSXxmWnlq5bLq2CUq4FrUQDdM=", + "zh:074f276975ffc873d8f9848d54073ef8320428828611d803c82b7c2559c696fb", + "zh:12bc0f45071b1af5d4c2beddd1ad54c3d91f246c04a41d51570fed2f56d4e7f2", + "zh:2310eac5e8a0286d11a830f33b9d7b93804a02abb63874d8ff9f08b11cc015ed", + "zh:43d70d5a760afd0b4d7d21a852ea4b507c6a6673a2ecd135b6991097bae723ce", + "zh:44d0fb42b80504497c0983f34135c7619a7f7dcd22ed7ef3c916c4d444ee73d5", + "zh:663d82298c96decffc9617183d3d1d5b36fa4aa3e7922897cbed2ca7766c7609", + "zh:9b81cc5347409b8f99fbc5ac289e0f2c82a4904615919001555303621791729f", + "zh:bc532772de1286cc931b6f672044f71d6be66a9ea81961c38b544495c9d6d765", + "zh:c6d1c975bc55a1bd3729daa5bbb7153ae664e2086ed1acf8781581f547b1dce9", + "zh:caaa3ebbdcc74205622f3cd3544860989295fba63a62c1e74f5f5161bdf81d53", + "zh:e71df7cf923bf5a8b11ddce562266904505d5dd3eb25d3797bdb308940ad5890", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:fdad54c5e50751cef3f39a8666ff6adbb3bd860d396d5a9a0a3526e204f60454", ] } diff --git a/terraform/infrastructure/gcp/main.tf b/terraform/infrastructure/gcp/main.tf index f381955227..33c359b68b 100644 --- a/terraform/infrastructure/gcp/main.tf +++ b/terraform/infrastructure/gcp/main.tf @@ -2,7 +2,12 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" + } + + google-beta = { + source = "hashicorp/google-beta" + version = "5.23.0" } random = { @@ -18,6 +23,12 @@ provider "google" { zone = var.zone } +provider "google-beta" { + project = var.project + region = var.region + zone = var.zone +} + locals { uid = random_id.uid.hex name = "${var.name}-${local.uid}" @@ -175,6 +186,7 @@ module "instance_group" { labels = local.labels init_secret_hash = local.init_secret_hash custom_endpoint = var.custom_endpoint + cc_technology = var.cc_technology } resource "google_compute_address" "loadbalancer_ip_internal" { diff --git a/terraform/infrastructure/gcp/modules/instance_group/main.tf b/terraform/infrastructure/gcp/modules/instance_group/main.tf index 2681c4d47c..fe9da14ae7 100644 --- a/terraform/infrastructure/gcp/modules/instance_group/main.tf +++ b/terraform/infrastructure/gcp/modules/instance_group/main.tf @@ -2,7 +2,12 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" + } + + google-beta = { + source = "hashicorp/google-beta" + version = "5.23.0" } random = { @@ -23,6 +28,10 @@ resource "random_id" "uid" { } resource "google_compute_instance_template" "template" { + # Beta provider is necessary to set confidential instance types. + # TODO(msanft): Remove beta provider once confidential instance type setting is in GA. + provider = google-beta + name = local.name machine_type = var.instance_type tags = ["constellation-${var.uid}"] // Note that this is also applied as a label @@ -33,8 +42,13 @@ resource "google_compute_instance_template" "template" { confidential_instance_config { enable_confidential_compute = true + confidential_instance_type = var.cc_technology } + # If SEV-SNP is used, we have to explicitly select a Milan processor, as per + # https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance_template#confidential_instance_type + min_cpu_platform = var.cc_technology == "SEV_SNP" ? "AMD Milan" : null + disk { disk_size_gb = 10 source_image = var.image_id diff --git a/terraform/infrastructure/gcp/modules/instance_group/variables.tf b/terraform/infrastructure/gcp/modules/instance_group/variables.tf index f4b9a7cdb4..5370ec7d15 100644 --- a/terraform/infrastructure/gcp/modules/instance_group/variables.tf +++ b/terraform/infrastructure/gcp/modules/instance_group/variables.tf @@ -99,3 +99,12 @@ variable "custom_endpoint" { type = string description = "Custom endpoint to use for the Kubernetes API server. If not set, the default endpoint will be used." } + +variable "cc_technology" { + type = string + description = "The confidential computing technology to use for the nodes. One of `SEV`, `SEV_SNP`." + validation { + condition = contains(["SEV", "SEV_SNP"], var.cc_technology) + error_message = "The confidential computing technology has to be 'SEV' or 'SEV_SNP'." + } +} diff --git a/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf b/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf index 2589ba1beb..263ee12a37 100644 --- a/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf +++ b/terraform/infrastructure/gcp/modules/internal_load_balancer/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/infrastructure/gcp/modules/jump_host/main.tf b/terraform/infrastructure/gcp/modules/jump_host/main.tf index a0a2e4c4f7..c1929792b7 100644 --- a/terraform/infrastructure/gcp/modules/jump_host/main.tf +++ b/terraform/infrastructure/gcp/modules/jump_host/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/infrastructure/gcp/modules/loadbalancer/main.tf b/terraform/infrastructure/gcp/modules/loadbalancer/main.tf index 0a5074f535..5c7bab447d 100644 --- a/terraform/infrastructure/gcp/modules/loadbalancer/main.tf +++ b/terraform/infrastructure/gcp/modules/loadbalancer/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/infrastructure/gcp/variables.tf b/terraform/infrastructure/gcp/variables.tf index add9eeffa8..5d158c9ad4 100644 --- a/terraform/infrastructure/gcp/variables.tf +++ b/terraform/infrastructure/gcp/variables.tf @@ -60,3 +60,12 @@ variable "zone" { type = string description = "GCP zone to deploy the cluster in." } + +variable "cc_technology" { + type = string + description = "The confidential computing technology to use for the nodes. One of `SEV`, `SEV_SNP`." + validation { + condition = contains(["SEV", "SEV_SNP"], var.cc_technology) + error_message = "The confidential computing technology has to be 'SEV' or 'SEV_SNP'." + } +} diff --git a/terraform/infrastructure/iam/gcp/.terraform.lock.hcl b/terraform/infrastructure/iam/gcp/.terraform.lock.hcl index 2fcd905b42..3575f3cfe8 100644 --- a/terraform/infrastructure/iam/gcp/.terraform.lock.hcl +++ b/terraform/infrastructure/iam/gcp/.terraform.lock.hcl @@ -2,26 +2,26 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { - version = "5.17.0" - constraints = "5.17.0" + version = "5.23.0" + constraints = "5.23.0" hashes = [ - "h1:9DKCaGp9EFKDLWIOWI3yA/RgWTMh0EMD6+iggVXC9l0=", - "h1:JEfDiodirnMqwNaub/anXoOtWt68aEN80QtPJxg3jsc=", - "h1:TANQI64JuScQ2LTITQqz7eh1RjhYDItdbI5p1aBOtXY=", - "h1:dT3UftIyARC7YjS4yurPlNS7WJAHICDHMXSluAAvavA=", - "h1:lu84RYioCT4OxXbFBdqom4QvSPAjMkEyHPSIAxuS7oo=", - "zh:31b4d485ee66e6ff2eb1d8e476e694904447ce2b7143a2e067e4b80a84958d13", - "zh:32e86a51c4b0b29b7a18dd95616ea2976f08a4a7385e00f2bcab266217ee4320", - "zh:357f352bf04e7bc10d61d49296bf6503f31a3db0500169cb532afde7d318643e", - "zh:4b4637ca397cc771136edf7ec5578b5ab8631a8955a86d4fce3b8c40ca8c26b4", - "zh:4fe198b7427f7bf04270a5491a0352379c2b0a1caf12e206e6e224ceb085f56a", - "zh:7abb8509a61602d5ed4c801e7cd7c8299d109bc07980352251ba79880a99abab", - "zh:b1550fe08c650d8419860da1568d3f77093d269f880cad7d720d843b2a9ec545", - "zh:c91d7079646a3fdbb927085e368a16b221a23c17cf7455d5088f0c8f5da48c9f", - "zh:d367213a5f392852ef0708283df583703b2efd0b44f9e599cd055086c371cf74", - "zh:d5b557f294f4094a865afaa0611dc2e657d485b60903f12795eeedc2e1c3aa87", + "h1:2VJTKCZWQ1DaNwclFxSo27avsYwWgq/itwLZ3xKyl/o=", + "h1:4evtipODvV5s86gihS+jyk1cSW1xLn22jy8Ox8zzhAs=", + "h1:BD+iQfFcZ0OeaZI2JWDp2sLqSr+DfZtWy4yo1OVMnTI=", + "h1:my3kqg4hIpWLu2WwRewOFxBS+FXfkAIiw8xTYVPNS9M=", + "h1:xpm8QPNp2soGqIEnf4SNoZaTlQ/SbNH63BooJkSbgX0=", + "zh:18eaaa51a8b30fed61c73799b8716a9bd08ccd382bc395c63e45b9a52ed8b300", + "zh:20c71acf091a282db88473ec6f0a684ac59891713c49b2ff1cb35c1539da3121", + "zh:2e3e9ae1d3b045dcaa39053f4d1d066fa17e5b81f4ed7a5e57cc4e6e1e651900", + "zh:531d1552f251c5a0176543defa95c2cc259fc8b9359ef6fd3df404dcead555a0", + "zh:67a7800023fa09a7d87ac02231364988749663e37e2906aa89c70eecc5955ccf", + "zh:6a8076b59d2766a05ffe521cc115f3e8df7cd2ee4c6d60de4ee4636f47714f2e", + "zh:7b39fe720bb7a1f35cd0e4dfeff617338342fc2d16bb22274b42c080ff633140", + "zh:b181e04c32aa53ad78eaf6f2746ec5fd94977187ba7314ae8e9815ef6ea56532", + "zh:bf605be2f8942d5cabb8755ff0d18f243b53f1148f5f32db762667cf64bfa949", + "zh:e981988558310df5d94e56adaa76f7444d991357fe9600c46eb70fa61f4a1394", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:fdad54c5e50751cef3f39a8666ff6adbb3bd860d396d5a9a0a3526e204f60454", + "zh:f663776d79e7e5d131b4fbd68c152f2bef3e899a19c9baabe3a441e3f5e809ea", ] } diff --git a/terraform/infrastructure/iam/gcp/main.tf b/terraform/infrastructure/iam/gcp/main.tf index 899d448c94..38afbe1ca8 100644 --- a/terraform/infrastructure/iam/gcp/main.tf +++ b/terraform/infrastructure/iam/gcp/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = "5.17.0" + version = "5.23.0" } } } diff --git a/terraform/legacy-module/gcp-constellation/main.tf b/terraform/legacy-module/gcp-constellation/main.tf index 27c45b1ea7..3029f1fb33 100644 --- a/terraform/legacy-module/gcp-constellation/main.tf +++ b/terraform/legacy-module/gcp-constellation/main.tf @@ -41,6 +41,7 @@ module "gcp" { zone = var.zone debug = var.debug custom_endpoint = var.custom_endpoint + cc_technology = var.cc_technology } module "constellation" { diff --git a/terraform/legacy-module/gcp-constellation/variables.tf b/terraform/legacy-module/gcp-constellation/variables.tf index 92787bfd48..0087b4fba1 100644 --- a/terraform/legacy-module/gcp-constellation/variables.tf +++ b/terraform/legacy-module/gcp-constellation/variables.tf @@ -70,3 +70,12 @@ variable "internal_load_balancer" { default = false description = "Use an internal load balancer." } + +variable "cc_technology" { + type = string + description = "The confidential computing technology to use for the nodes. One of `SEV`, `SEV_SNP`." + validation { + condition = contains(["SEV", "SEV_SNP"], var.cc_technology) + error_message = "The confidential computing technology has to be 'SEV' or 'SEV_SNP'." + } +}