Support SEV-SNP on GCP (#3011)

* terraform: enable creation of SEV-SNP VMs on GCP

* variant: add SEV-SNP attestation variant

* config: add SEV-SNP config options for GCP

* measurements: add GCP SEV-SNP measurements

* gcp: separate package for SEV-ES

* attestation: add GCP SEV-SNP attestation logic

* gcp: factor out common logic

* choose: add GCP SEV-SNP

* cli: add TF variable passthrough for GCP SEV-SNP variables

* cli: support GCP SEV-SNP for `constellation verify`

* Adjust usage of GCP SEV-SNP throughout codebase

* ci: add GCP SEV-SNP

* terraform-provider: support GCP SEV-SNP

* docs: add GCP SEV-SNP reference

* linter fixes

* gcp: only run test with TPM simulator

* gcp: remove nonsense test

* Update cli/internal/cmd/verify.go

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>

* Update docs/docs/overview/clouds.md

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>

* Update terraform-provider-constellation/internal/provider/attestation_data_source_test.go

Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>

* linter fixes

* terraform_provider: correctly pass down CC technology

* config: mark attestationconfigapi as unimplemented

* gcp: fix comments and typos

* snp: use nonce and PK hash in SNP report

* snp: ensure we never use ARK supplied by Issuer (#3025)

* Make sure SNP ARK is always loaded from config, or fetched from AMD KDS
* GCP: Set validator `reportData` correctly

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
Co-authored-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* attestationconfigapi: add GCP to uploading

* snp: use correct cert

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* terraform-provider: enable fetching of attestation config values for GCP SEV-SNP

* linter fixes

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>
Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>
This commit is contained in:
Moritz Sanft 2024-04-16 18:13:47 +02:00 committed by GitHub
parent 485ebb151e
commit 913b09aeb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 1623 additions and 552 deletions

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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 <<EOF
locals {
project_id = "constellation-e2e"
region = "${region}"
zone = "${{ inputs.regionZone || 'europe-west3-b' }}"
cc_technology = "${cc_tech}"
}
EOF
cat _override.tf

View File

@ -49,6 +49,10 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
- test: "sonobuoy full"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "sonobuoy full"
attestationVariant: "azure-sev-snp"
@ -72,6 +76,11 @@ jobs:
kubernetes-version: "v1.28"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "sonobuoy full"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.28"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "sonobuoy full"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.28"
@ -93,6 +102,11 @@ jobs:
kubernetes-version: "v1.27"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "sonobuoy full"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.27"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "sonobuoy full"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.27"
@ -115,6 +129,11 @@ jobs:
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "verify"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "verify"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.29"
@ -137,6 +156,11 @@ jobs:
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "recover"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "recover"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.29"
@ -159,6 +183,11 @@ jobs:
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "lb"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "lb"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.29"
@ -181,6 +210,11 @@ jobs:
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "autoscaling"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "autoscaling"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.29"
@ -203,6 +237,11 @@ jobs:
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "perf-bench"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
runner: "ubuntu-22.04"
clusterCreation: "cli"
- test: "perf-bench"
attestationVariant: "azure-sev-snp"
kubernetes-version: "v1.29"
@ -223,6 +262,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "malicious join"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "malicious join"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"

View File

@ -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:

View File

@ -57,6 +57,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "sonobuoy full"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "sonobuoy full"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -79,6 +84,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.28"
clusterCreation: "cli"
- test: "sonobuoy quick"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.28"
clusterCreation: "cli"
- test: "sonobuoy quick"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -100,6 +110,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.27"
clusterCreation: "cli"
- test: "sonobuoy quick"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.27"
clusterCreation: "cli"
- test: "sonobuoy quick"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -123,6 +138,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "verify"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "verify"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -146,6 +166,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "recover"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "recover"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -168,6 +193,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "lb"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "lb"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -190,6 +220,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "autoscaling"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "autoscaling"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -212,6 +247,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "perf-bench"
refStream: "ref/main/stream/debug/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.29"
clusterCreation: "cli"
- test: "perf-bench"
refStream: "ref/main/stream/debug/?"
attestationVariant: "azure-sev-snp"
@ -241,6 +281,11 @@ jobs:
attestationVariant: "gcp-sev-es"
kubernetes-version: "v1.28"
clusterCreation: "cli"
- test: "verify"
refStream: "ref/release/stream/stable/?"
attestationVariant: "gcp-sev-snp"
kubernetes-version: "v1.28"
clusterCreation: "cli"
- test: "verify"
refStream: "ref/release/stream/stable/?"
attestationVariant: "azure-sev-snp"

View File

@ -12,6 +12,7 @@ on:
type: choice
options:
- "gcp-sev-es"
- "gcp-sev-snp"
- "azure-sev-snp"
- "azure-tdx"
- "aws-sev-snp"

View File

@ -7,10 +7,11 @@ on:
description: "Which attestation variant to use."
type: choice
options:
- "gcp-sev-es"
- "aws-sev-snp"
- "azure-sev-snp"
- "azure-tdx"
- "aws-sev-snp"
- "gcp-sev-es"
- "gcp-sev-snp"
default: "azure-sev-snp"
required: true
nodeCount:

View File

@ -161,6 +161,7 @@ jobs:
id: fetch-reference
shell: bash
run: |
# TODO(msanft): Implement marketplace images for GCP SEV-SNP
aws s3 cp s3://cdn-constellation-backend/constellation/v2/ref/-/stream/stable/${{ steps.fetch-version.outputs.output }}/image/info.json .
FULL_REF=$(yq e -r -oy '.list.[] | select(.attestationVariant == "gcp-sev-es") | .reference' info.json)
IMAGE_NAME=$(echo "${FULL_REF}" | cut -d / -f 5)

View File

@ -209,6 +209,12 @@ func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPCluste
DiskType: group.StateDiskType,
}
}
ccTech := "SEV"
if conf.GetAttestationConfig().GetVariant().Equal(variant.GCPSEVSNP{}) {
ccTech = "SEV_SNP"
}
return &terraform.GCPClusterVariables{
Name: conf.Name,
NodeGroups: nodeGroups,
@ -219,6 +225,7 @@ func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPCluste
Debug: conf.IsDebugCluster(),
CustomEndpoint: conf.CustomEndpoint,
InternalLoadBalancer: conf.InternalLoadBalancer,
CCTechnology: ccTech,
}
}

View File

@ -235,6 +235,11 @@ func TestValidProviderAttestationCombination(t *testing.T) {
variant.GCPSEVES{},
config.AttestationConfig{GCPSEVES: defaultAttestation.GCPSEVES},
},
{
cloudprovider.GCP,
variant.GCPSEVSNP{},
config.AttestationConfig{GCPSEVSNP: defaultAttestation.GCPSEVSNP},
},
{
cloudprovider.QEMU,
variant.QEMUVTPM{},
@ -286,6 +291,10 @@ func TestParseAttestationFlag(t *testing.T) {
attestationFlag: "gcp-sev-es",
wantVariant: variant.GCPSEVES{},
},
"GCPSEVSNP": {
attestationFlag: "gcp-sev-snp",
wantVariant: variant.GCPSEVSNP{},
},
"QEMUVTPM": {
attestationFlag: "qemu-vtpm",
wantVariant: variant.QEMUVTPM{},

View File

@ -107,8 +107,11 @@ func runVerify(cmd *cobra.Command, _ []string) error {
log: log,
}
formatterFactory := func(output string, attestation variant.Variant, log debugLog) (attestationDocFormatter, error) {
if output == "json" && (!attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{})) {
return nil, errors.New("json output is only supported for Azure SEV-SNP and AWS SEV-SNP")
if output == "json" &&
(!attestation.Equal(variant.AzureSEVSNP{}) &&
!attestation.Equal(variant.AWSSEVSNP{}) &&
!attestation.Equal(variant.GCPSEVSNP{})) {
return nil, errors.New("json output is only supported for SEV-SNP")
}
switch output {
case "json":
@ -467,7 +470,7 @@ func updateInitMeasurements(config config.AttestationCfg, ownerID, clusterID str
switch config.GetVariant() {
case variant.AWSNitroTPM{}, variant.AWSSEVSNP{},
variant.AzureTrustedLaunch{}, variant.AzureSEVSNP{}, variant.AzureTDX{}, // AzureTDX also uses a vTPM for measurements
variant.GCPSEVES{},
variant.GCPSEVES{}, variant.GCPSEVSNP{},
variant.QEMUVTPM{}:
if err := updateMeasurementTPM(m, uint32(measurements.PCRIndexOwnerID), ownerID); err != nil {
return err

View File

@ -136,6 +136,8 @@ type GCPClusterVariables struct {
CustomEndpoint string `hcl:"custom_endpoint" cty:"custom_endpoint"`
// InternalLoadBalancer is true if an internal load balancer should be created.
InternalLoadBalancer bool `hcl:"internal_load_balancer" cty:"internal_load_balancer"`
// CCTechnology is the confidential computing technology to use on the VMs. (`SEV` or `SEV_SNP`)
CCTechnology string `hcl:"cc_technology" cty:"cc_technology"`
}
// GetCreateMAA gets the CreateMAA variable.

View File

@ -122,6 +122,7 @@ func TestGCPClusterVariables(t *testing.T) {
},
},
CustomEndpoint: "example.com",
CCTechnology: "SEV_SNP",
}
// test that the variables are correctly rendered
@ -151,6 +152,7 @@ node_groups = {
}
custom_endpoint = "example.com"
internal_load_balancer = false
cc_technology = "SEV_SNP"
`
got := vars.String()
assert.Equal(t, strings.Fields(want), strings.Fields(got)) // to ignore whitespace differences

View File

@ -78,7 +78,7 @@ constellation config generate {aws|azure|gcp|openstack|qemu|stackit} [flags]
### Options
```
-a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-tdx|azure-trustedlaunch|gcp-sev-es|qemu-vtpm}. If not specified, the default for the cloud provider is used
-a, --attestation string attestation variant to use {aws-sev-snp|aws-nitro-tpm|azure-sev-snp|azure-tdx|azure-trustedlaunch|gcp-sev-es|gcp-sev-snp|qemu-vtpm}. If not specified, the default for the cloud provider is used
-h, --help help for generate
-k, --kubernetes string Kubernetes version to use in format MAJOR.MINOR (default "v1.28")
```

View File

@ -15,7 +15,7 @@ information contained in the objects. Especially the paths used for the API are
in these helper methods.
Regarding the decision to implement new types over using the existing types from internal/config:
AttesationCfg objects for AttestationCfg API need to hold some version information (for sorting, recognizing latest).
AttestationCfg objects for AttestationCfg API need to hold some version information (for sorting, recognizing latest).
Thus, existing config types (AWSNitroTPM, AzureSEVSNP, ...) can not be extended to implement apiObject interface.
Instead, we need a separate type that wraps _all_ attestation types. In the codebase this is done using the AttestationCfg interface.
The new type AttestationCfgGet needs to be located inside internal/config in order to implement UnmarshalJSON.

View File

@ -10,8 +10,6 @@ go_binary(
go_library(
name = "cli_lib",
srcs = [
"aws.go",
"azure.go",
"delete.go",
"main.go",
"upload.go",
@ -28,7 +26,7 @@ go_library(
"//internal/logger",
"//internal/staticupload",
"//internal/verify",
"@com_github_aws_aws_sdk_go//aws",
"@com_github_aws_aws_sdk_go_v2//aws",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//types",
"@com_github_spf13_afero//:afero",

View File

@ -1,24 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
import (
"context"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
)
func deleteAWS(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error {
if cfg.provider != cloudprovider.AWS || cfg.kind != snpReport {
return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind)
}
return client.DeleteSEVSNPVersion(ctx, variant.AWSSEVSNP{}, cfg.version)
}

View File

@ -1,61 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go/aws"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
)
func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error {
if cfg.provider != cloudprovider.Azure && cfg.kind != snpReport {
return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind)
}
return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version)
}
func deleteRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error {
resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(cfg.bucket),
Prefix: aws.String(path),
})
if err != nil {
return err
}
// Delete all objects in the path.
objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents))
for i, obj := range resp.Contents {
objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key}
}
if len(objIDs) > 0 {
_, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: aws.String(cfg.bucket),
Delete: &s3types.Delete{
Objects: objIDs,
Quiet: toPtr(true),
},
})
if err != nil {
return err
}
}
return nil
}
func toPtr[T any](v T) *T {
return &v
}

View File

@ -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} <version>",
Use: "delete {aws|azure|gcp} {snp-report|guest-firmware} <version>",
Short: "Delete an object from the attestationconfig API",
Long: "Delete a specific object version from the config api. <version> 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/<csp>",
Long: "Delete all objects from the API path constellation/v1/attestation/<csp>",
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
}

View File

@ -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

View File

@ -26,7 +26,7 @@ import (
func newUploadCmd() *cobra.Command {
uploadCmd := &cobra.Command{
Use: "upload {azure|aws} {snp-report|guest-firmware} <path>",
Use: "upload {aws|azure|gcp} {snp-report|guest-firmware} <path>",
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,

View File

@ -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)
}

View File

@ -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{

View File

@ -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"`
}

View File

@ -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{

View File

@ -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
}

View File

@ -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",

View File

@ -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:

View File

@ -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{},
},

View File

@ -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",
],
)

View File

@ -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",
],
)

View File

@ -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

View File

@ -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,
),
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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":

View File

@ -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()

View File

@ -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}}

View File

@ -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),

View File

@ -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",
],
)

View File

@ -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",

View File

@ -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": {

View File

@ -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{}

View File

@ -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))
}

View File

@ -10,6 +10,7 @@ go_library(
"azure.go",
"config.go",
"config_doc.go",
"gcp.go",
# keep
"image_enterprise.go",
# keep

View File

@ -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{}:

View File

@ -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{})},
},

View File

@ -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.

View File

@ -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,

View File

@ -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,

128
internal/config/gcp.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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(

View File

@ -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")

View File

@ -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`
<a id="nestedatt--attestation--azure_firmware_signer_config"></a>

View File

@ -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.

View File

@ -111,6 +111,7 @@ Required:
* `azure-sev-snp`
* `azure-tdx`
* `gcp-sev-es`
* `gcp-sev-snp`
* `qemu-vtpm`
Optional:

View File

@ -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" {

View File

@ -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())

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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"),
},
}
}

View File

@ -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",
]
}

View File

@ -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" {

View File

@ -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

View File

@ -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'."
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.17.0"
version = "5.23.0"
}
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.17.0"
version = "5.23.0"
}
}
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.17.0"
version = "5.23.0"
}
}
}

View File

@ -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'."
}
}

View File

@ -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",
]
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.17.0"
version = "5.23.0"
}
}
}

View File

@ -41,6 +41,7 @@ module "gcp" {
zone = var.zone
debug = var.debug
custom_endpoint = var.custom_endpoint
cc_technology = var.cc_technology
}
module "constellation" {

View File

@ -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'."
}
}