api: add support to upload AWS TCB values

The attestationconfig api CLI now uploads SNP TCB
versions for AWS.
This commit is contained in:
Otto Bittner 2023-11-14 13:24:25 +01:00
parent 4813fcfdb6
commit 67348792dc
11 changed files with 334 additions and 320 deletions

View File

@ -1,5 +1,4 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
load("//bazel/sh:def.bzl", "sh_template") load("//bazel/sh:def.bzl", "sh_template")
go_binary( go_binary(
@ -15,6 +14,7 @@ go_library(
"azure.go", "azure.go",
"delete.go", "delete.go",
"main.go", "main.go",
"upload.go",
"validargs.go", "validargs.go",
], ],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli", importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli",
@ -37,15 +37,6 @@ go_library(
], ],
) )
go_test(
name = "cli_test",
srcs = [
"delete_test.go",
"main_test.go",
],
embed = [":cli_lib"],
)
sh_template( sh_template(
name = "cli_e2e_test", name = "cli_e2e_test",
data = [":cli"], data = [":cli"],

View File

@ -8,16 +8,17 @@ package main
import ( import (
"context" "context"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
) )
func uploadAWS(_ context.Context, _ *attestationconfigapi.Client, _ uploadConfig, _ file.Handler, _ *logger.Logger) error { func deleteAWS(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error {
return nil if cfg.provider != cloudprovider.AWS || cfg.kind != snpReport {
} return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind)
}
func deleteAWS(_ context.Context, _ *attestationconfigapi.Client, _ deleteConfig) error { return client.DeleteSEVSNPVersion(ctx, variant.AWSSEVSNP{}, cfg.version)
return nil
} }

View File

@ -8,7 +8,6 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
@ -17,65 +16,18 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload" "github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/edgelesssys/constellation/v2/internal/verify"
) )
func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg uploadConfig, fs file.Handler, log *logger.Logger) error {
if cfg.kind != snpReport {
return fmt.Errorf("kind %s not supported", cfg.kind)
}
log.Infof("Reading SNP report from file: %s", cfg.path)
var report verify.Report
if err := fs.ReadJSON(cfg.path, &report); err != nil {
return fmt.Errorf("reading snp report: %w", err)
}
inputVersion := convertTCBVersionToAzureVersion(report.SNPReport.LaunchTCB)
log.Infof("Input report: %+v", inputVersion)
latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{})
if err != nil {
if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) {
log.Infof("No versions found in API, but assuming that we are uploading the first version.")
} else {
return fmt.Errorf("fetching latest version: %w", err)
}
}
latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion
if err := client.UploadSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil {
if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) {
log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion)
return nil
}
return fmt.Errorf("updating latest version: %w", err)
}
return nil
}
func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.SEVSNPVersion {
return attestationconfigapi.SEVSNPVersion{
Bootloader: tcb.Bootloader,
TEE: tcb.TEE,
SNP: tcb.SNP,
Microcode: tcb.Microcode,
}
}
func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error {
if cfg.provider == cloudprovider.Azure && cfg.kind == snpReport { if cfg.provider != cloudprovider.Azure && cfg.kind != snpReport {
return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version) return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind)
} }
return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version)
} }
func deleteRecursiveAzure(ctx context.Context, client *staticupload.Client, cfg deleteConfig) error { func deleteRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error {
path := "constellation/v1/attestation/azure-sev-snp"
resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(cfg.bucket), Bucket: aws.String(cfg.bucket),
Prefix: aws.String(path), Prefix: aws.String(path),

View File

@ -8,8 +8,10 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"path"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "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/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload" "github.com/edgelesssys/constellation/v2/internal/staticupload"
@ -21,19 +23,21 @@ import (
func newDeleteCmd() *cobra.Command { func newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "delete {azure|aws} {snp-report|guest-firmware} <version>", Use: "delete {azure|aws} {snp-report|guest-firmware} <version>",
Short: "Upload an object to the attestationconfig API", 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)", 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",
Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)), Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)),
PreRunE: envCheck, PreRunE: envCheck,
RunE: runDelete, RunE: runDelete,
} }
recursivelyCmd := &cobra.Command{ recursivelyCmd := &cobra.Command{
Use: "recursive {azure|aws} {snp-report|guest-firmware}", Use: "recursive {azure|aws}",
Short: "delete all objects from the API path constellation/v1/attestation/azure-sev-snp", Short: "delete all objects from the API path constellation/v1/attestation/<csp>",
Long: "Currently only implemented for azure & snp-report. Delete all objects from the API path constellation/v1/attestation/azure-sev-snp", Long: "Delete all objects from the API path constellation/v1/attestation/<csp>",
Args: cobra.MatchAll(cobra.ExactArgs(2), isCloudProvider(0), isValidKind(1)), Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete recursive azure",
RunE: runRecursiveDelete, Args: cobra.MatchAll(cobra.ExactArgs(1), isCloudProvider(0)),
RunE: runRecursiveDelete,
} }
cmd.AddCommand(recursivelyCmd) cmd.AddCommand(recursivelyCmd)
@ -41,6 +45,79 @@ func newDeleteCmd() *cobra.Command {
return cmd return cmd
} }
func runDelete(cmd *cobra.Command, args []string) (retErr error) {
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
deleteCfg, err := newDeleteConfig(cmd, ([3]string)(args[:3]))
if err != nil {
return fmt.Errorf("creating delete config: %w", err)
}
cfg := staticupload.Config{
Bucket: deleteCfg.bucket,
Region: deleteCfg.region,
DistributionID: deleteCfg.distribution,
}
client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg,
[]byte(cosignPwd), []byte(privateKey), false, 1, log)
if err != nil {
return fmt.Errorf("create attestation client: %w", err)
}
defer func() {
err := clientClose(cmd.Context())
if err != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err))
}
}()
switch deleteCfg.provider {
case cloudprovider.AWS:
return deleteAWS(cmd.Context(), client, deleteCfg)
case cloudprovider.Azure:
return deleteAzure(cmd.Context(), client, deleteCfg)
default:
return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider)
}
}
func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) {
// newDeleteConfig expects 3 args, so we pass "all" for the version argument and "snp-report" as kind.
args = append(args, "snp-report")
args = append(args, "all")
deleteCfg, err := newDeleteConfig(cmd, ([3]string)(args[:3]))
if err != nil {
return fmt.Errorf("creating delete config: %w", err)
}
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
client, closeFn, err := staticupload.New(cmd.Context(), staticupload.Config{
Bucket: deleteCfg.bucket,
Region: deleteCfg.region,
DistributionID: deleteCfg.distribution,
}, log)
if err != nil {
return fmt.Errorf("create static upload client: %w", err)
}
defer func() {
err := closeFn(cmd.Context())
if err != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to close client: %w", err))
}
}()
var deletePath string
switch deleteCfg.provider {
case cloudprovider.AWS:
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AWSSEVSNP{}.String())
case cloudprovider.Azure:
deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AzureSEVSNP{}.String())
default:
return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider)
}
return deleteRecursive(cmd.Context(), deletePath, client, deleteCfg)
}
type deleteConfig struct { type deleteConfig struct {
provider cloudprovider.Provider provider cloudprovider.Provider
kind objectKind kind objectKind
@ -84,69 +161,3 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) {
cosignPublicKey: apiCfg.cosignPublicKey, cosignPublicKey: apiCfg.cosignPublicKey,
}, nil }, nil
} }
func runDelete(cmd *cobra.Command, args []string) (retErr error) {
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
deleteCfg, err := newDeleteConfig(cmd, ([3]string)(args[:3]))
if err != nil {
return fmt.Errorf("creating delete config: %w", err)
}
cfg := staticupload.Config{
Bucket: deleteCfg.bucket,
Region: deleteCfg.region,
DistributionID: deleteCfg.distribution,
}
client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg,
[]byte(cosignPwd), []byte(privateKey), false, 1, log)
if err != nil {
return fmt.Errorf("create attestation client: %w", err)
}
defer func() {
err := clientClose(cmd.Context())
if err != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err))
}
}()
switch deleteCfg.provider {
case cloudprovider.AWS:
return deleteAWS(cmd.Context(), client, deleteCfg)
case cloudprovider.Azure:
return deleteAzure(cmd.Context(), client, deleteCfg)
default:
return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider)
}
}
func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) {
// newDeleteConfig expects 3 args, so we pass "all" for the version argument.
args = append(args, "all")
deleteCfg, err := newDeleteConfig(cmd, ([3]string)(args[:3]))
if err != nil {
return fmt.Errorf("creating delete config: %w", err)
}
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
client, closeFn, err := staticupload.New(cmd.Context(), staticupload.Config{
Bucket: deleteCfg.bucket,
Region: deleteCfg.region,
DistributionID: deleteCfg.distribution,
}, log)
if err != nil {
return fmt.Errorf("create static upload client: %w", err)
}
defer func() {
err := closeFn(cmd.Context())
if err != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to close client: %w", err))
}
}()
if deleteCfg.provider != cloudprovider.Azure || deleteCfg.kind != snpReport {
return fmt.Errorf("provider %s and kind %s not supported", deleteCfg.provider, deleteCfg.kind)
}
return deleteRecursiveAzure(cmd.Context(), client, deleteCfg)
}

View File

@ -1,6 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main

View File

@ -28,7 +28,7 @@ readonly tmpdir
registerExitHandler "rm -rf $tmpdir" registerExitHandler "rm -rf $tmpdir"
# empty the bucket version state # empty the bucket version state
${configapi_cli} delete recursive azure snp-report --region "$region" --bucket "$bucket" ${configapi_cli} delete recursive azure --region "$region" --bucket "$bucket"
# the high version numbers ensure that it's newer than the current latest value # the high version numbers ensure that it's newer than the current latest value
readonly current_report_path="$tmpdir/currentSnpReport.json" readonly current_report_path="$tmpdir/currentSnpReport.json"

View File

@ -15,20 +15,10 @@ Any version update is then pushed to the API.
package main package main
import ( import (
"errors"
"fmt"
"os" "os"
"time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap"
) )
const ( const (
@ -56,7 +46,10 @@ func main() {
// newRootCmd creates the root command. // newRootCmd creates the root command.
func newRootCmd() *cobra.Command { func newRootCmd() *cobra.Command {
rootCmd := &cobra.Command{} rootCmd := &cobra.Command{
Short: "CLI to interact with the attestationconfig API",
Long: "CLI to interact with the attestationconfig API. Allows uploading new TCB versions, deleting specific versions and deleting all versions. Uploaded objects are signed with cosign.",
}
rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.") rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.")
rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.") rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.")
rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.") rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.")
@ -67,156 +60,6 @@ func newRootCmd() *cobra.Command {
return rootCmd return rootCmd
} }
func newUploadCmd() *cobra.Command {
uploadCmd := &cobra.Command{
Use: "upload {azure|aws} {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."+
"The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+
"For guest-firmware objects the object is added to the API directly. "+
"Please authenticate with AWS through your preferred method (e.g. environment variables, CLI)"+
"to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.",
envCosignPrivateKey, envCosignPwd,
),
Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)),
PreRunE: envCheck,
RunE: runUpload,
}
uploadCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.")
uploadCmd.Flags().BoolP("force", "f", false, "Use force to manually push a new latest version."+
" The version gets saved to the cache but the version selection logic is skipped.")
uploadCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.")
return uploadCmd
}
func envCheck(_ *cobra.Command, _ []string) error {
if os.Getenv(envCosignPrivateKey) == "" || os.Getenv(envCosignPwd) == "" {
return fmt.Errorf("please set both %s and %s environment variables", envCosignPrivateKey, envCosignPwd)
}
cosignPwd = os.Getenv(envCosignPwd)
privateKey = os.Getenv(envCosignPrivateKey)
return nil
}
func runUpload(cmd *cobra.Command, args []string) (retErr error) {
ctx := cmd.Context()
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
log.Infof("%s", args)
uploadCfg, err := newConfig(cmd, ([3]string)(args[:3]))
if err != nil {
return fmt.Errorf("parsing cli flags: %w", err)
}
client, clientClose, err := attestationconfigapi.NewClient(
ctx,
staticupload.Config{
Bucket: uploadCfg.bucket,
Region: uploadCfg.region,
DistributionID: uploadCfg.distribution,
},
[]byte(cosignPwd),
[]byte(privateKey),
false,
uploadCfg.cacheWindowSize,
log)
defer func() {
err := clientClose(cmd.Context())
if err != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err))
}
}()
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
switch uploadCfg.provider {
case cloudprovider.AWS:
return uploadAWS(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
case cloudprovider.Azure:
return uploadAzure(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
default:
return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider)
}
}
type uploadConfig struct {
provider cloudprovider.Provider
kind objectKind
path string
uploadDate time.Time
cosignPublicKey string
region string
bucket string
distribution string
url string
force bool
cacheWindowSize int
}
func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) {
dateStr, err := cmd.Flags().GetString("upload-date")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting upload date: %w", err)
}
uploadDate := time.Now()
if dateStr != "" {
uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr)
if err != nil {
return uploadConfig{}, fmt.Errorf("parsing date: %w", err)
}
}
region, err := cmd.Flags().GetString("region")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting region: %w", err)
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting bucket: %w", err)
}
testing, err := cmd.Flags().GetBool("testing")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting testing flag: %w", err)
}
apiCfg := getAPIEnvironment(testing)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting force: %w", err)
}
cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err)
}
provider := cloudprovider.FromString(args[0])
kind := kindFromString(args[1])
path := args[2]
return uploadConfig{
provider: provider,
kind: kind,
path: path,
uploadDate: uploadDate,
cosignPublicKey: apiCfg.cosignPublicKey,
region: region,
bucket: bucket,
url: apiCfg.url,
distribution: apiCfg.distribution,
force: force,
cacheWindowSize: cacheWindowSize,
}, nil
}
type apiConfig struct { type apiConfig struct {
url string url string
distribution string distribution string

View File

@ -1,6 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main

View File

@ -0,0 +1,228 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"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/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/edgelesssys/constellation/v2/internal/verify"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func newUploadCmd() *cobra.Command {
uploadCmd := &cobra.Command{
Use: "upload {azure|aws} {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."+
"The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+
"For guest-firmware objects the object is added to the API directly. "+
"Please authenticate with AWS through your preferred method (e.g. environment variables, CLI)"+
"to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.",
envCosignPrivateKey, envCosignPwd,
),
Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli upload azure snp-report /some/path/report.json",
Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)),
PreRunE: envCheck,
RunE: runUpload,
}
uploadCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.")
uploadCmd.Flags().BoolP("force", "f", false, "Use force to manually push a new latest version."+
" The version gets saved to the cache but the version selection logic is skipped.")
uploadCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.")
return uploadCmd
}
func envCheck(_ *cobra.Command, _ []string) error {
if os.Getenv(envCosignPrivateKey) == "" || os.Getenv(envCosignPwd) == "" {
return fmt.Errorf("please set both %s and %s environment variables", envCosignPrivateKey, envCosignPwd)
}
cosignPwd = os.Getenv(envCosignPwd)
privateKey = os.Getenv(envCosignPrivateKey)
return nil
}
func runUpload(cmd *cobra.Command, args []string) (retErr error) {
ctx := cmd.Context()
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
uploadCfg, err := newConfig(cmd, ([3]string)(args[:3]))
if err != nil {
return fmt.Errorf("parsing cli flags: %w", err)
}
client, clientClose, err := attestationconfigapi.NewClient(
ctx,
staticupload.Config{
Bucket: uploadCfg.bucket,
Region: uploadCfg.region,
DistributionID: uploadCfg.distribution,
},
[]byte(cosignPwd),
[]byte(privateKey),
false,
uploadCfg.cacheWindowSize,
log)
defer func() {
err := clientClose(cmd.Context())
if err != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err))
}
}()
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
var attesation variant.Variant
switch uploadCfg.provider {
case cloudprovider.AWS:
attesation = variant.AWSSEVSNP{}
case cloudprovider.Azure:
attesation = variant.AzureSEVSNP{}
default:
return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider)
}
return uploadReport(ctx, attesation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
}
func uploadReport(ctx context.Context,
attestation variant.Variant,
client *attestationconfigapi.Client,
cfg uploadConfig,
fs file.Handler,
log *logger.Logger,
) error {
if cfg.kind != snpReport {
return fmt.Errorf("kind %s not supported", cfg.kind)
}
log.Infof("Reading SNP report from file: %s", cfg.path)
var report verify.Report
if err := fs.ReadJSON(cfg.path, &report); err != nil {
return fmt.Errorf("reading snp report: %w", err)
}
inputVersion := convertTCBVersionToSNPVersion(report.SNPReport.LaunchTCB)
log.Infof("Input report: %+v", inputVersion)
latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchSEVSNPVersionLatest(ctx, attestation)
if err != nil {
if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) {
log.Infof("No versions found in API, but assuming that we are uploading the first version.")
} else {
return fmt.Errorf("fetching latest version: %w", err)
}
}
latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion
if err := client.UploadSEVSNPVersionLatest(ctx, attestation, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil {
if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) {
log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion)
return nil
}
return fmt.Errorf("updating latest version: %w", err)
}
return nil
}
func convertTCBVersionToSNPVersion(tcb verify.TCBVersion) attestationconfigapi.SEVSNPVersion {
return attestationconfigapi.SEVSNPVersion{
Bootloader: tcb.Bootloader,
TEE: tcb.TEE,
SNP: tcb.SNP,
Microcode: tcb.Microcode,
}
}
type uploadConfig struct {
provider cloudprovider.Provider
kind objectKind
path string
uploadDate time.Time
cosignPublicKey string
region string
bucket string
distribution string
url string
force bool
cacheWindowSize int
}
func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) {
dateStr, err := cmd.Flags().GetString("upload-date")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting upload date: %w", err)
}
uploadDate := time.Now()
if dateStr != "" {
uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr)
if err != nil {
return uploadConfig{}, fmt.Errorf("parsing date: %w", err)
}
}
region, err := cmd.Flags().GetString("region")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting region: %w", err)
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting bucket: %w", err)
}
testing, err := cmd.Flags().GetBool("testing")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting testing flag: %w", err)
}
apiCfg := getAPIEnvironment(testing)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting force: %w", err)
}
cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size")
if err != nil {
return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err)
}
provider := cloudprovider.FromString(args[0])
kind := kindFromString(args[1])
path := args[2]
return uploadConfig{
provider: provider,
kind: kind,
path: path,
uploadDate: uploadDate,
cosignPublicKey: apiCfg.cosignPublicKey,
region: region,
bucket: bucket,
url: apiCfg.url,
distribution: apiCfg.distribution,
force: force,
cacheWindowSize: cacheWindowSize,
}, nil
}

View File

@ -34,7 +34,7 @@ const cachedVersionsSubDir = "cached-versions"
var ErrNoNewerVersion = errors.New("input version is not newer than latest API version") var ErrNoNewerVersion = errors.New("input version is not newer than latest API version")
func reportVersionDir(attestation variant.Variant) string { func reportVersionDir(attestation variant.Variant) string {
return path.Join(attestationURLPath, attestation.String(), cachedVersionsSubDir) return path.Join(AttestationURLPath, attestation.String(), cachedVersionsSubDir)
} }
// UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest // UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest

View File

@ -16,8 +16,8 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/variant"
) )
// attestationURLPath is the URL path to the attestation versions. // AttestationURLPath is the URL path to the attestation versions.
const attestationURLPath = "constellation/v1/attestation" 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 Azure SEVSNP.
type SEVSNPVersion struct { type SEVSNPVersion struct {
@ -43,7 +43,7 @@ type SEVSNPVersionAPI struct {
// JSONPath returns the path to the JSON file for the request to the config api. // JSONPath returns the path to the JSON file for the request to the config api.
func (i SEVSNPVersionAPI) JSONPath() string { func (i SEVSNPVersionAPI) JSONPath() string {
return path.Join(attestationURLPath, i.Variant.String(), i.Version) return path.Join(AttestationURLPath, i.Variant.String(), i.Version)
} }
// ValidateRequest validates the request. // ValidateRequest validates the request.
@ -83,7 +83,7 @@ func (i SEVSNPVersionList) List() []string { return i.list }
// JSONPath returns the path to the JSON file for the request to the config api. // JSONPath returns the path to the JSON file for the request to the config api.
func (i SEVSNPVersionList) JSONPath() string { func (i SEVSNPVersionList) JSONPath() string {
return path.Join(attestationURLPath, i.variant.String(), "list") return path.Join(AttestationURLPath, i.variant.String(), "list")
} }
// ValidateRequest is a NoOp as there is no input. // ValidateRequest is a NoOp as there is no input.