Support SEV-SNP on GCP (#3011)

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

* variant: add SEV-SNP attestation variant

* config: add SEV-SNP config options for GCP

* measurements: add GCP SEV-SNP measurements

* gcp: separate package for SEV-ES

* attestation: add GCP SEV-SNP attestation logic

* gcp: factor out common logic

* choose: add GCP SEV-SNP

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

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

* Adjust usage of GCP SEV-SNP throughout codebase

* ci: add GCP SEV-SNP

* terraform-provider: support GCP SEV-SNP

* docs: add GCP SEV-SNP reference

* linter fixes

* gcp: only run test with TPM simulator

* gcp: remove nonsense test

* Update cli/internal/cmd/verify.go

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

* Update docs/docs/overview/clouds.md

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

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

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

* linter fixes

* terraform_provider: correctly pass down CC technology

* config: mark attestationconfigapi as unimplemented

* gcp: fix comments and typos

* snp: use nonce and PK hash in SNP report

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

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

---------

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

* attestationconfigapi: add GCP to uploading

* snp: use correct cert

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

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

* linter fixes

---------

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,11 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"path"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
@ -22,7 +26,7 @@ import (
// newDeleteCmd creates the delete command.
func newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete {azure|aws} {snp-report|guest-firmware} <version>",
Use: "delete {aws|azure|gcp} {snp-report|guest-firmware} <version>",
Short: "Delete an object from the attestationconfig API",
Long: "Delete a specific object version from the config api. <version> is the name of the object to delete (without .json suffix)",
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete azure snp-report 1.0.0",
@ -32,7 +36,7 @@ func newDeleteCmd() *cobra.Command {
}
recursivelyCmd := &cobra.Command{
Use: "recursive {azure|aws}",
Use: "recursive {aws|azure|gcp}",
Short: "delete all objects from the API path constellation/v1/attestation/<csp>",
Long: "Delete all objects from the API path constellation/v1/attestation/<csp>",
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete recursive azure",
@ -72,9 +76,11 @@ func runDelete(cmd *cobra.Command, args []string) (retErr error) {
switch deleteCfg.provider {
case cloudprovider.AWS:
return deleteAWS(cmd.Context(), client, deleteCfg)
return deleteEntry(cmd.Context(), variant.AWSSEVSNP{}, client, deleteCfg)
case cloudprovider.Azure:
return deleteAzure(cmd.Context(), client, deleteCfg)
return deleteEntry(cmd.Context(), variant.AzureSEVSNP{}, client, deleteCfg)
case cloudprovider.GCP:
return deleteEntry(cmd.Context(), variant.GCPSEVSNP{}, client, deleteCfg)
default:
return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider)
}
@ -111,11 +117,13 @@ func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) {
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AWSSEVSNP{}.String())
case cloudprovider.Azure:
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AzureSEVSNP{}.String())
case cloudprovider.GCP:
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.GCPSEVSNP{}.String())
default:
return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider)
}
return deleteRecursive(cmd.Context(), deletePath, client, deleteCfg)
return deleteEntryRecursive(cmd.Context(), deletePath, client, deleteCfg)
}
type deleteConfig struct {
@ -161,3 +169,44 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) {
cosignPublicKey: apiCfg.cosignPublicKey,
}, nil
}
func deleteEntry(ctx context.Context, attvar variant.Variant, client *attestationconfigapi.Client, cfg deleteConfig) error {
if cfg.kind != snpReport {
return fmt.Errorf("kind %s not supported", cfg.kind)
}
return client.DeleteSEVSNPVersion(ctx, attvar, cfg.version)
}
func deleteEntryRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error {
resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(cfg.bucket),
Prefix: aws.String(path),
})
if err != nil {
return err
}
// Delete all objects in the path.
objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents))
for i, obj := range resp.Contents {
objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key}
}
if len(objIDs) > 0 {
_, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: aws.String(cfg.bucket),
Delete: &s3types.Delete{
Objects: objIDs,
Quiet: toPtr(true),
},
})
if err != nil {
return err
}
}
return nil
}
func toPtr[T any](v T) *T {
return &v
}

View file

@ -26,6 +26,9 @@ function variant() {
elif [[ $1 == "azure" ]]; then
echo "azure-sev-snp"
return 0
elif [[ $1 == "gcp" ]]; then
echo "gcp-sev-snp"
return 0
else
echo "Unknown CSP: $1"
exit 1

View file

@ -26,7 +26,7 @@ import (
func newUploadCmd() *cobra.Command {
uploadCmd := &cobra.Command{
Use: "upload {azure|aws} {snp-report|guest-firmware} <path>",
Use: "upload {aws|azure|gcp} {snp-report|guest-firmware} <path>",
Short: "Upload an object to the attestationconfig API",
Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+
@ -92,17 +92,19 @@ func runUpload(cmd *cobra.Command, args []string) (retErr error) {
return fmt.Errorf("creating client: %w", err)
}
var attesation variant.Variant
var attestation variant.Variant
switch uploadCfg.provider {
case cloudprovider.AWS:
attesation = variant.AWSSEVSNP{}
attestation = variant.AWSSEVSNP{}
case cloudprovider.Azure:
attesation = variant.AzureSEVSNP{}
attestation = variant.AzureSEVSNP{}
case cloudprovider.GCP:
attestation = variant.GCPSEVSNP{}
default:
return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider)
}
return uploadReport(ctx, attesation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
return uploadReport(ctx, attestation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
}
func uploadReport(ctx context.Context,

View file

@ -48,7 +48,7 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK
return repo, clientClose, nil
}
// uploadSEVSNPVersion uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix.
// uploadSEVSNPVersion uploads the latest version numbers of the SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix.
func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error {
versions, err := a.List(ctx, attestation)
if err != nil {
@ -75,7 +75,9 @@ func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Var
// List returns the list of versions for the given attestation variant.
func (a Client) List(ctx context.Context, attestation variant.Variant) (SEVSNPVersionList, error) {
if !attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{}) {
if !attestation.Equal(variant.AzureSEVSNP{}) &&
!attestation.Equal(variant.AWSSEVSNP{}) &&
!attestation.Equal(variant.GCPSEVSNP{}) {
return SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation)
}

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
*/
/*
The reporter contains the logic to determine a latest version for Azure SEVSNP based on cached version values observed on CVM instances.
The reporter contains the logic to determine a latest version for SEVSNP based on cached version values observed on CVM instances.
Some code in this file (e.g. listing cached files) does not rely on dedicated API objects and instead uses the AWS SDK directly,
for no other reason than original development speed.
*/
@ -79,11 +79,11 @@ func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation varia
if err := c.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil {
return fmt.Errorf("uploading version: %w", err)
}
c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion))
c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new SEV-SNP version: %+v", minVersion))
return nil
}
// cacheSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API.
// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API.
func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error {
dateStr := date.Format(VersionFormat) + ".json"
res := putCmd{

View file

@ -19,15 +19,15 @@ import (
// AttestationURLPath is the URL path to the attestation versions.
const AttestationURLPath = "constellation/v1/attestation"
// SEVSNPVersion tracks the latest version of each component of the Azure SEVSNP.
// SEVSNPVersion tracks the latest version of each component of the SEVSNP.
type SEVSNPVersion struct {
// Bootloader is the latest version of the Azure SEVSNP bootloader.
// Bootloader is the latest version of the SEVSNP bootloader.
Bootloader uint8 `json:"bootloader"`
// TEE is the latest version of the Azure SEVSNP TEE.
// TEE is the latest version of the SEVSNP TEE.
TEE uint8 `json:"tee"`
// SNP is the latest version of the Azure SEVSNP SNP.
// SNP is the latest version of the SEVSNP SNP.
SNP uint8 `json:"snp"`
// Microcode is the latest version of the Azure SEVSNP microcode.
// Microcode is the latest version of the SEVSNP microcode.
Microcode uint8 `json:"microcode"`
}