From d4e5ffcea9fbb05f5ddf4d6a3692b4918ce63189 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:08:11 +0200 Subject: [PATCH] attestationconfigapi: add GCP to uploading --- .github/actions/e2e_verify/action.yml | 2 +- .../workflows/e2e-attestationconfigapi.yml | 2 +- .github/workflows/e2e-test-weekly.yml | 5 ++ cli/internal/cmd/verify.go | 7 ++- .../attestationconfigapi.go | 2 +- internal/api/attestationconfigapi/cli/aws.go | 24 -------- .../api/attestationconfigapi/cli/azure.go | 61 ------------------- .../api/attestationconfigapi/cli/delete.go | 53 +++++++++++++++- .../attestationconfigapi/cli/e2e/test.sh.in | 3 + .../api/attestationconfigapi/cli/upload.go | 12 ++-- internal/api/attestationconfigapi/client.go | 6 +- internal/api/attestationconfigapi/reporter.go | 6 +- internal/api/attestationconfigapi/snp.go | 10 +-- internal/attestation/gcp/snp/issuer.go | 6 +- internal/config/gcp.go | 58 ++++++++---------- 15 files changed, 115 insertions(+), 142 deletions(-) delete mode 100644 internal/api/attestationconfigapi/cli/aws.go delete mode 100644 internal/api/attestationconfigapi/cli/azure.go diff --git a/.github/actions/e2e_verify/action.yml b/.github/actions/e2e_verify/action.yml index aca4fdceb4..c52d02f43b 100644 --- a/.github/actions/e2e_verify/action.yml +++ b/.github/actions/e2e_verify/action.yml @@ -84,7 +84,7 @@ runs: aws-region: eu-central-1 - name: Upload extracted TCBs - if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'aws-sev-snp') + if: github.ref_name == 'main' && (inputs.attestationVariant == 'azure-sev-snp' || inputs.attestationVariant == 'aws-sev-snp' || inputs.attestationVariant == 'gcp-sev-snp') shell: bash env: COSIGN_PASSWORD: ${{ inputs.cosignPassword }} diff --git a/.github/workflows/e2e-attestationconfigapi.yml b/.github/workflows/e2e-attestationconfigapi.yml index a3605dafce..e02c1d4db1 100644 --- a/.github/workflows/e2e-attestationconfigapi.yml +++ b/.github/workflows/e2e-attestationconfigapi.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false max-parallel: 1 matrix: - csp: ["azure", "aws"] + csp: ["azure", "aws", "gcp"] runs-on: ubuntu-22.04 permissions: id-token: write diff --git a/.github/workflows/e2e-test-weekly.yml b/.github/workflows/e2e-test-weekly.yml index fff7600840..f656775ab5 100644 --- a/.github/workflows/e2e-test-weekly.yml +++ b/.github/workflows/e2e-test-weekly.yml @@ -281,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" diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 37b6726f6f..5a84c3df0d 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -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": diff --git a/internal/api/attestationconfigapi/attestationconfigapi.go b/internal/api/attestationconfigapi/attestationconfigapi.go index e170da869b..a0d84d7863 100644 --- a/internal/api/attestationconfigapi/attestationconfigapi.go +++ b/internal/api/attestationconfigapi/attestationconfigapi.go @@ -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. diff --git a/internal/api/attestationconfigapi/cli/aws.go b/internal/api/attestationconfigapi/cli/aws.go deleted file mode 100644 index 578caadc4f..0000000000 --- a/internal/api/attestationconfigapi/cli/aws.go +++ /dev/null @@ -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) -} diff --git a/internal/api/attestationconfigapi/cli/azure.go b/internal/api/attestationconfigapi/cli/azure.go deleted file mode 100644 index a10fb4e131..0000000000 --- a/internal/api/attestationconfigapi/cli/azure.go +++ /dev/null @@ -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 -} diff --git a/internal/api/attestationconfigapi/cli/delete.go b/internal/api/attestationconfigapi/cli/delete.go index d0b0f447f6..61cd5240c4 100644 --- a/internal/api/attestationconfigapi/cli/delete.go +++ b/internal/api/attestationconfigapi/cli/delete.go @@ -6,11 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only package main import ( + "context" "errors" "fmt" "log/slog" "path" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" @@ -72,9 +76,9 @@ 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) default: return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider) } @@ -111,11 +115,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 +167,44 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) { cosignPublicKey: apiCfg.cosignPublicKey, }, nil } + +func deleteEntry(ctx context.Context, attvar variant.Variant, client *attestationconfigapi.Client, cfg deleteConfig) error { + if cfg.kind != snpReport { + return fmt.Errorf("kind %s not supported", cfg.kind) + } + + return client.DeleteSEVSNPVersion(ctx, attvar, cfg.version) +} + +func deleteEntryRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error { + resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: aws.String(cfg.bucket), + Prefix: aws.String(path), + }) + if err != nil { + return err + } + + // Delete all objects in the path. + objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents)) + for i, obj := range resp.Contents { + objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key} + } + if len(objIDs) > 0 { + _, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: aws.String(cfg.bucket), + Delete: &s3types.Delete{ + Objects: objIDs, + Quiet: toPtr(true), + }, + }) + if err != nil { + return err + } + } + return nil +} + +func toPtr[T any](v T) *T { + return &v +} diff --git a/internal/api/attestationconfigapi/cli/e2e/test.sh.in b/internal/api/attestationconfigapi/cli/e2e/test.sh.in index 773443df45..5fb23f06f7 100755 --- a/internal/api/attestationconfigapi/cli/e2e/test.sh.in +++ b/internal/api/attestationconfigapi/cli/e2e/test.sh.in @@ -26,6 +26,9 @@ function variant() { elif [[ $1 == "azure" ]]; then echo "azure-sev-snp" return 0 + elif [[ $1 == "gcp" ]]; then + echo "gcp-sev-snp" + return 0 else echo "Unknown CSP: $1" exit 1 diff --git a/internal/api/attestationconfigapi/cli/upload.go b/internal/api/attestationconfigapi/cli/upload.go index 54036009ab..98303cae2d 100644 --- a/internal/api/attestationconfigapi/cli/upload.go +++ b/internal/api/attestationconfigapi/cli/upload.go @@ -26,7 +26,7 @@ import ( func newUploadCmd() *cobra.Command { uploadCmd := &cobra.Command{ - Use: "upload {azure|aws} {snp-report|guest-firmware} ", + Use: "upload {aws|azure|gcp} {snp-report|guest-firmware} ", Short: "Upload an object to the attestationconfig API", Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+ @@ -92,17 +92,19 @@ func runUpload(cmd *cobra.Command, args []string) (retErr error) { return fmt.Errorf("creating client: %w", err) } - var attesation variant.Variant + var attestation variant.Variant switch uploadCfg.provider { case cloudprovider.AWS: - attesation = variant.AWSSEVSNP{} + attestation = variant.AWSSEVSNP{} case cloudprovider.Azure: - attesation = variant.AzureSEVSNP{} + attestation = variant.AzureSEVSNP{} + case cloudprovider.GCP: + attestation = variant.GCPSEVSNP{} default: return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider) } - return uploadReport(ctx, attesation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) + return uploadReport(ctx, attestation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) } func uploadReport(ctx context.Context, diff --git a/internal/api/attestationconfigapi/client.go b/internal/api/attestationconfigapi/client.go index 583e3bba4d..11b27eae0b 100644 --- a/internal/api/attestationconfigapi/client.go +++ b/internal/api/attestationconfigapi/client.go @@ -48,7 +48,7 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK return repo, clientClose, nil } -// uploadSEVSNPVersion uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. +// uploadSEVSNPVersion uploads the latest version numbers of the SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { versions, err := a.List(ctx, attestation) if err != nil { @@ -75,7 +75,9 @@ func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Var // List returns the list of versions for the given attestation variant. func (a Client) List(ctx context.Context, attestation variant.Variant) (SEVSNPVersionList, error) { - if !attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{}) { + if !attestation.Equal(variant.AzureSEVSNP{}) && + !attestation.Equal(variant.AWSSEVSNP{}) && + !attestation.Equal(variant.GCPSEVSNP{}) { return SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation) } diff --git a/internal/api/attestationconfigapi/reporter.go b/internal/api/attestationconfigapi/reporter.go index 00656e8816..72a9803479 100644 --- a/internal/api/attestationconfigapi/reporter.go +++ b/internal/api/attestationconfigapi/reporter.go @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only */ /* -The reporter contains the logic to determine a latest version for Azure SEVSNP based on cached version values observed on CVM instances. +The reporter contains the logic to determine a latest version for SEVSNP based on cached version values observed on CVM instances. Some code in this file (e.g. listing cached files) does not rely on dedicated API objects and instead uses the AWS SDK directly, for no other reason than original development speed. */ @@ -79,11 +79,11 @@ func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation varia if err := c.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil { return fmt.Errorf("uploading version: %w", err) } - c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion)) + c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new SEV-SNP version: %+v", minVersion)) return nil } -// cacheSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API. +// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API. func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { dateStr := date.Format(VersionFormat) + ".json" res := putCmd{ diff --git a/internal/api/attestationconfigapi/snp.go b/internal/api/attestationconfigapi/snp.go index 68098a3ad6..a0f92700b6 100644 --- a/internal/api/attestationconfigapi/snp.go +++ b/internal/api/attestationconfigapi/snp.go @@ -19,15 +19,15 @@ import ( // AttestationURLPath is the URL path to the attestation versions. const AttestationURLPath = "constellation/v1/attestation" -// SEVSNPVersion tracks the latest version of each component of the Azure SEVSNP. +// SEVSNPVersion tracks the latest version of each component of the SEVSNP. type SEVSNPVersion struct { - // Bootloader is the latest version of the Azure SEVSNP bootloader. + // Bootloader is the latest version of the SEVSNP bootloader. Bootloader uint8 `json:"bootloader"` - // TEE is the latest version of the Azure SEVSNP TEE. + // TEE is the latest version of the SEVSNP TEE. TEE uint8 `json:"tee"` - // SNP is the latest version of the Azure SEVSNP SNP. + // SNP is the latest version of the SEVSNP SNP. SNP uint8 `json:"snp"` - // Microcode is the latest version of the Azure SEVSNP microcode. + // Microcode is the latest version of the SEVSNP microcode. Microcode uint8 `json:"microcode"` } diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go index 806baa2619..0ceb5e9601 100644 --- a/internal/attestation/gcp/snp/issuer.go +++ b/internal/attestation/gcp/snp/issuer.go @@ -62,8 +62,8 @@ func getInstanceInfo(_ context.Context, _ io.ReadWriteCloser, extraData []byte) if len(extraData) > 64 { return nil, fmt.Errorf("extra data too long: %d, should be 64 bytes at most", len(extraData)) } - truncatedExtraData := make([]byte, 64) - copy(truncatedExtraData, extraData) + extraData64 := make([]byte, 64) + copy(extraData64, extraData) device, err := sevclient.OpenDevice() if err != nil { @@ -71,7 +71,7 @@ func getInstanceInfo(_ context.Context, _ io.ReadWriteCloser, extraData []byte) } defer device.Close() - report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, [64]byte(truncatedExtraData), 0) + report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, [64]byte(extraData64), 0) if err != nil { return nil, fmt.Errorf("getting extended report: %w", err) } diff --git a/internal/config/gcp.go b/internal/config/gcp.go index b4cc590c66..847474f05b 100644 --- a/internal/config/gcp.go +++ b/internal/config/gcp.go @@ -73,39 +73,35 @@ func (c *GCPSEVSNP) getToMarshallLatestWithResolvedVersions() AttestationCfg { } // FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. -func (c *GCPSEVSNP) FetchAndSetLatestVersionNumbers(_ context.Context, _ attestationconfigapi.Fetcher) error { - panic("not implemented") - - // TODO(msanft): Implement with https://dev.azure.com/Edgeless/Edgeless/_workitems/edit/4024 - - // // 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) 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 -// } -// } +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 {