mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
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:
parent
485ebb151e
commit
913b09aeb8
2
.github/actions/e2e_verify/action.yml
vendored
2
.github/actions/e2e_verify/action.yml
vendored
@ -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 }}
|
||||
|
3
.github/actions/terraform_apply/action.yml
vendored
3
.github/actions/terraform_apply/action.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
2
.github/workflows/e2e-test-daily.yml
vendored
2
.github/workflows/e2e-test-daily.yml
vendored
@ -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
|
||||
|
5
.github/workflows/e2e-test-internal-lb.yml
vendored
5
.github/workflows/e2e-test-internal-lb.yml
vendored
@ -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:
|
||||
|
@ -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:
|
||||
|
11
.github/workflows/e2e-test-provider-example.yml
vendored
11
.github/workflows/e2e-test-provider-example.yml
vendored
@ -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
|
||||
|
44
.github/workflows/e2e-test-release.yml
vendored
44
.github/workflows/e2e-test-release.yml
vendored
@ -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"
|
||||
|
@ -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:
|
||||
|
45
.github/workflows/e2e-test-weekly.yml
vendored
45
.github/workflows/e2e-test-weekly.yml
vendored
@ -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"
|
||||
|
1
.github/workflows/e2e-test.yml
vendored
1
.github/workflows/e2e-test.yml
vendored
@ -12,6 +12,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- "gcp-sev-es"
|
||||
- "gcp-sev-snp"
|
||||
- "azure-sev-snp"
|
||||
- "azure-tdx"
|
||||
- "aws-sev-snp"
|
||||
|
5
.github/workflows/e2e-upgrade.yml
vendored
5
.github/workflows/e2e-upgrade.yml
vendored
@ -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:
|
||||
|
1
.github/workflows/on-release.yml
vendored
1
.github/workflows/on-release.yml
vendored
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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{},
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
```
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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{},
|
||||
},
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
43
internal/attestation/gcp/es/BUILD.bazel
Normal file
43
internal/attestation/gcp/es/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
45
internal/attestation/gcp/es/es.go
Normal file
45
internal/attestation/gcp/es/es.go
Normal 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
|
33
internal/attestation/gcp/es/issuer.go
Normal file
33
internal/attestation/gcp/es/issuer.go
Normal 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,
|
||||
),
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
59
internal/attestation/gcp/es/validator.go
Normal file
59
internal/attestation/gcp/es/validator.go
Normal 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
|
||||
}
|
@ -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,
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
69
internal/attestation/gcp/metadata.go
Normal file
69
internal/attestation/gcp/metadata.go
Normal 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()
|
||||
}
|
101
internal/attestation/gcp/restclient.go
Normal file
101
internal/attestation/gcp/restclient.go
Normal 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
|
||||
}
|
29
internal/attestation/gcp/snp/BUILD.bazel
Normal file
29
internal/attestation/gcp/snp/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
168
internal/attestation/gcp/snp/issuer.go
Normal file
168
internal/attestation/gcp/snp/issuer.go
Normal 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
|
||||
}
|
42
internal/attestation/gcp/snp/snp.go
Normal file
42
internal/attestation/gcp/snp/snp.go
Normal 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
|
206
internal/attestation/gcp/snp/validator.go
Normal file
206
internal/attestation/gcp/snp/validator.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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":
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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}}
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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{}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ go_library(
|
||||
"azure.go",
|
||||
"config.go",
|
||||
"config_doc.go",
|
||||
"gcp.go",
|
||||
# keep
|
||||
"image_enterprise.go",
|
||||
# keep
|
||||
|
@ -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{}:
|
||||
|
@ -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{})},
|
||||
},
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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
128
internal/config/gcp.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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")
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -111,6 +111,7 @@ Required:
|
||||
* `azure-sev-snp`
|
||||
* `azure-tdx`
|
||||
* `gcp-sev-es`
|
||||
* `gcp-sev-snp`
|
||||
* `qemu-vtpm`
|
||||
|
||||
Optional:
|
||||
|
@ -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" {
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -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" {
|
||||
|
@ -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
|
||||
|
@ -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'."
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "5.17.0"
|
||||
version = "5.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "5.17.0"
|
||||
version = "5.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "5.17.0"
|
||||
version = "5.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'."
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "5.17.0"
|
||||
version = "5.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ module "gcp" {
|
||||
zone = var.zone
|
||||
debug = var.debug
|
||||
custom_endpoint = var.custom_endpoint
|
||||
cc_technology = var.cc_technology
|
||||
}
|
||||
|
||||
module "constellation" {
|
||||
|
@ -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'."
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user