api: refactor attestationcfgapi cli

The cli now takes CSP and object kind as argument.
Also made upload an explicit command and the report
path/version an argument.
Previously the report was a flag. The CSP was hardcoded.
There was only one object kind (snp-report).
This commit is contained in:
Otto Bittner 2023-11-09 09:59:19 +01:00
parent 84d8bd8110
commit 5542f9c63c
10 changed files with 333 additions and 247 deletions

View File

@ -93,5 +93,5 @@ runs:
for file in $(ls snp-report-*.json); do for file in $(ls snp-report-*.json); do
path=$(realpath "${file}") path=$(realpath "${file}")
cat "${path}" cat "${path}"
bazel run //internal/api/attestationconfigapi/cli -- --snp-report-path "${path}" bazel run //internal/api/attestationconfigapi/cli -- upload azure snp-report "${path}"
done done

View File

@ -11,13 +11,18 @@ go_binary(
go_library( go_library(
name = "cli_lib", name = "cli_lib",
srcs = [ srcs = [
"aws.go",
"azure.go",
"delete.go", "delete.go",
"main.go", "main.go",
"objectkind_string.go",
"validargs.go",
], ],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli", importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli",
visibility = ["//visibility:private"], visibility = ["//visibility:private"],
deps = [ deps = [
"//internal/api/attestationconfigapi", "//internal/api/attestationconfigapi",
"//internal/cloud/cloudprovider",
"//internal/constants", "//internal/constants",
"//internal/file", "//internal/file",
"//internal/logger", "//internal/logger",
@ -40,9 +45,9 @@ go_test(
], ],
embed = [":cli_lib"], embed = [":cli_lib"],
deps = [ deps = [
"//internal/cloud/cloudprovider",
"//internal/verify", "//internal/verify",
"@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
], ],
) )

View File

@ -0,0 +1,23 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
import (
"context"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
)
func uploadAWS(_ context.Context, _ *attestationconfigapi.Client, _ uploadConfig, _ file.Handler, _ *logger.Logger) error {
return nil
}
func deleteAWS(_ context.Context, _ *attestationconfigapi.Client, _ deleteConfig) error {
return nil
}

View File

@ -0,0 +1,104 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
import (
"context"
"errors"
"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/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"
)
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).FetchAzureSEVSNPVersionLatest(ctx)
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.AzureSEVSNPVersion
if err := client.UploadAzureSEVSNPVersionLatest(ctx, 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.AzureSEVSNPVersion {
return attestationconfigapi.AzureSEVSNPVersion{
Bootloader: tcb.Bootloader,
TEE: tcb.TEE,
SNP: tcb.SNP,
Microcode: tcb.Microcode,
}
}
func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error {
if cfg.provider == cloudprovider.Azure && cfg.kind == snpReport {
return client.DeleteAzureSEVSNPVersion(ctx, cfg.version)
}
return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind)
}
func deleteRecursiveAzure(ctx context.Context, client *staticupload.Client, cfg deleteConfig) error {
path := "constellation/v1/attestation/azure-sev-snp"
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: true,
},
})
if err != nil {
return err
}
}
return nil
}

View File

@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only
package main package main
import ( import (
"context"
"errors" "errors"
"fmt" "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/api/attestationconfigapi"
"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"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -23,62 +20,83 @@ import (
// newDeleteCmd creates the delete command. // newDeleteCmd creates the delete command.
func newDeleteCmd() *cobra.Command { func newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "delete", Use: "delete {azure|aws} {snp-report|guest-firmware} <version>",
Short: "delete a specific version from the config api", Short: "Upload an object to 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)",
Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)),
PreRunE: envCheck, PreRunE: envCheck,
RunE: runDelete, RunE: runDelete,
} }
cmd.Flags().StringP("version", "v", "", "Name of the version to delete (without .json suffix)")
must(cmd.MarkFlagRequired("version"))
recursivelyCmd := &cobra.Command{ recursivelyCmd := &cobra.Command{
Use: "recursive", Use: "recursive {azure|aws} {snp-report|guest-firmware}",
Short: "delete all objects from the API path", Short: "delete all objects from the API path constellation/v1/attestation/azure-sev-snp",
Long: "Currently only implemented for azure & snp-report. Delete all objects from the API path constellation/v1/attestation/azure-sev-snp",
Args: cobra.MatchAll(cobra.ExactArgs(2), isCloudProvider(0), isValidKind(1)),
RunE: runRecursiveDelete, RunE: runRecursiveDelete,
} }
cmd.AddCommand(recursivelyCmd) cmd.AddCommand(recursivelyCmd)
return cmd return cmd
} }
type deleteCmd struct { type deleteConfig struct {
attestationClient deleteClient provider cloudprovider.Provider
kind objectKind
version string
region string
bucket string
url string
distribution string
cosignPublicKey string
} }
type deleteClient interface { func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) {
DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error
}
func (d deleteCmd) delete(cmd *cobra.Command) error {
version, err := cmd.Flags().GetString("version")
if err != nil {
return err
}
return d.attestationClient.DeleteAzureSEVSNPVersion(cmd.Context(), version)
}
func runDelete(cmd *cobra.Command, _ []string) (retErr error) {
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
region, err := cmd.Flags().GetString("region") region, err := cmd.Flags().GetString("region")
if err != nil { if err != nil {
return fmt.Errorf("getting region: %w", err) return deleteConfig{}, fmt.Errorf("getting region: %w", err)
} }
bucket, err := cmd.Flags().GetString("bucket") bucket, err := cmd.Flags().GetString("bucket")
if err != nil { if err != nil {
return fmt.Errorf("getting bucket: %w", err) return deleteConfig{}, fmt.Errorf("getting bucket: %w", err)
} }
testing, err := cmd.Flags().GetBool("testing") testing, err := cmd.Flags().GetBool("testing")
if err != nil { if err != nil {
return fmt.Errorf("getting testing flag: %w", err) return deleteConfig{}, fmt.Errorf("getting testing flag: %w", err)
} }
apiCfg := getAPIEnvironment(testing) apiCfg := getAPIEnvironment(testing)
provider := cloudprovider.FromString(args[0])
kind := kindFromString(args[1])
version := args[2]
return deleteConfig{
provider: provider,
kind: kind,
version: version,
region: region,
bucket: bucket,
url: apiCfg.url,
distribution: apiCfg.distribution,
cosignPublicKey: apiCfg.cosignPublicKey,
}, 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{ cfg := staticupload.Config{
Bucket: bucket, Bucket: deleteCfg.bucket,
Region: region, Region: deleteCfg.region,
DistributionID: apiCfg.distribution, DistributionID: deleteCfg.distribution,
} }
client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg, client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg,
[]byte(cosignPwd), []byte(privateKey), false, 1, log) []byte(cosignPwd), []byte(privateKey), false, 1, log)
@ -92,34 +110,29 @@ func runDelete(cmd *cobra.Command, _ []string) (retErr error) {
} }
}() }()
deleteCmd := deleteCmd{ switch deleteCfg.provider {
attestationClient: client, 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)
} }
return deleteCmd.delete(cmd)
} }
func runRecursiveDelete(cmd *cobra.Command, _ []string) (retErr error) { func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) {
region, err := cmd.Flags().GetString("region") // 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 { if err != nil {
return fmt.Errorf("getting region: %w", err) return fmt.Errorf("creating delete config: %w", err)
} }
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return fmt.Errorf("getting bucket: %w", err)
}
testing, err := cmd.Flags().GetBool("testing")
if err != nil {
return fmt.Errorf("getting testing flag: %w", err)
}
apiCfg := getAPIEnvironment(testing)
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
client, closeFn, err := staticupload.New(cmd.Context(), staticupload.Config{ client, closeFn, err := staticupload.New(cmd.Context(), staticupload.Config{
Bucket: bucket, Bucket: deleteCfg.bucket,
Region: region, Region: deleteCfg.region,
DistributionID: apiCfg.distribution, DistributionID: deleteCfg.distribution,
}, log) }, log)
if err != nil { if err != nil {
return fmt.Errorf("create static upload client: %w", err) return fmt.Errorf("create static upload client: %w", err)
@ -130,31 +143,10 @@ func runRecursiveDelete(cmd *cobra.Command, _ []string) (retErr error) {
retErr = errors.Join(retErr, fmt.Errorf("failed to close client: %w", err)) retErr = errors.Join(retErr, fmt.Errorf("failed to close client: %w", err))
} }
}() }()
path := "constellation/v1/attestation/azure-sev-snp"
resp, err := client.ListObjectsV2(cmd.Context(), &s3.ListObjectsV2Input{ if deleteCfg.provider != cloudprovider.Azure || deleteCfg.kind != snpReport {
Bucket: aws.String(bucket), return fmt.Errorf("provider %s and kind %s not supported", deleteCfg.provider, deleteCfg.kind)
Prefix: aws.String(path),
})
if err != nil {
return err
} }
// Delete all objects in the path. return deleteRecursiveAzure(cmd.Context(), client, deleteCfg)
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(cmd.Context(), &s3.DeleteObjectsInput{
Bucket: aws.String(bucket),
Delete: &s3types.Delete{
Objects: objIDs,
Quiet: true,
},
})
if err != nil {
return err
}
}
return nil
} }

View File

@ -4,35 +4,3 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package main package main
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeleteVersion(t *testing.T) {
client := &fakeAttestationClient{}
sut := deleteCmd{
attestationClient: client,
}
cmd := newDeleteCmd()
require.NoError(t, cmd.Flags().Set("version", "2021-01-01"))
assert.NoError(t, sut.delete(cmd))
assert.True(t, client.isCalled)
}
type fakeAttestationClient struct {
isCalled bool
}
func (f *fakeAttestationClient) DeleteAzureSEVSNPVersion(_ context.Context, version string) error {
if version == "2021-01-01" {
f.isCalled = true
return nil
}
return errors.New("version does not exist")
}

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 --region "$region" --bucket "$bucket" ${configapi_cli} delete recursive azure snp-report --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"
@ -57,7 +57,7 @@ cat << EOF > "$current_report_path"
} }
EOF EOF
# upload a fake latest version for the fetcher # upload a fake latest version for the fetcher
${configapi_cli} --force --snp-report-path "$current_report_path" --upload-date "2000-01-01-01-01" --region "$region" --bucket "$bucket" ${configapi_cli} upload azure snp-report "$current_report_path" --force --upload-date "2000-01-01-01-01" --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 report_path="$tmpdir/snpReport.json" readonly report_path="$tmpdir/snpReport.json"
@ -115,11 +115,11 @@ EOF
# report 3 versions with different dates to fill the reporter cache # report 3 versions with different dates to fill the reporter cache
readonly date_oldest="2023-02-01-03-04" readonly date_oldest="2023-02-01-03-04"
${configapi_cli} --snp-report-path "$older_report_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3 ${configapi_cli} upload azure snp-report "$older_report_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3
readonly date_older="2023-02-02-03-04" readonly date_older="2023-02-02-03-04"
${configapi_cli} --snp-report-path "$older_report_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3 ${configapi_cli} upload azure snp-report "$older_report_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3
readonly date="2023-02-03-03-04" readonly date="2023-02-03-03-04"
${configapi_cli} --snp-report-path "$report_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3 ${configapi_cli} upload azure snp-report "$report_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3
# expect that $date_oldest is served as latest version # expect that $date_oldest is served as latest version
baseurl="https://d33dzgxuwsgbpw.cloudfront.net/constellation/v1/attestation/azure-sev-snp" baseurl="https://d33dzgxuwsgbpw.cloudfront.net/constellation/v1/attestation/azure-sev-snp"
@ -165,7 +165,7 @@ if [[ $http_code -ne 404 ]]; then
exit 1 exit 1
fi fi
${configapi_cli} delete --version "$date_oldest" --region "$region" --bucket "$bucket" ${configapi_cli} delete azure snp-report "$date_oldest" --region "$region" --bucket "$bucket"
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail. # Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_oldest}.json) http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_oldest}.json)

View File

@ -21,11 +21,11 @@ import (
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "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/file"
"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"
"github.com/edgelesssys/constellation/v2/internal/verify"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
@ -56,29 +56,40 @@ 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{}
Use: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY upload --version-file $FILE", rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.")
Short: "Upload a set of versions specific to the azure-sev-snp attestation variant to the config api.", rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.")
rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.")
Long: fmt.Sprintf("The CLI uploads an observed version number specific to the azure-sev-snp attestation variant to a cache directory. 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. "+ rootCmd.AddCommand(newUploadCmd())
rootCmd.AddCommand(newDeleteCmd())
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)"+ "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.", "to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.",
envCosignPrivateKey, envCosignPwd, envCosignPrivateKey, envCosignPwd,
), ),
Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)),
PreRunE: envCheck, PreRunE: envCheck,
RunE: runCmd, RunE: runUpload,
} }
rootCmd.Flags().StringP("snp-report-path", "t", "", "File path to a file containing the Constellation verify output.") uploadCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.")
rootCmd.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."+
rootCmd.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.") " The version gets saved to the cache but the version selection logic is skipped.")
rootCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.") uploadCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.")
rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.")
rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.") return uploadCmd
rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.")
must(rootCmd.MarkFlagRequired("snp-report-path"))
rootCmd.AddCommand(newDeleteCmd())
return rootCmd
} }
func envCheck(_ *cobra.Command, _ []string) error { func envCheck(_ *cobra.Command, _ []string) error {
@ -90,38 +101,29 @@ func envCheck(_ *cobra.Command, _ []string) error {
return nil return nil
} }
func runCmd(cmd *cobra.Command, _ []string) (retErr error) { func runUpload(cmd *cobra.Command, args []string) (retErr error) {
ctx := cmd.Context() ctx := cmd.Context()
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
flags, err := parseCliFlags(cmd) log.Infof("%s", args)
uploadCfg, err := newConfig(cmd, ([3]string)(args[:3]))
if err != nil { if err != nil {
return fmt.Errorf("parsing cli flags: %w", err) return fmt.Errorf("parsing cli flags: %w", err)
} }
cfg := staticupload.Config{ client, clientClose, err := attestationconfigapi.NewClient(
Bucket: flags.bucket, ctx,
Region: flags.region, staticupload.Config{
DistributionID: flags.distribution, Bucket: uploadCfg.bucket,
} Region: uploadCfg.region,
DistributionID: uploadCfg.distribution,
},
[]byte(cosignPwd),
[]byte(privateKey),
false,
uploadCfg.cacheWindowSize,
log)
log.Infof("Reading SNP report from file: %s", flags.snpReportPath)
fs := file.NewHandler(afero.NewOsFs())
var report verify.Report
if err := fs.ReadJSON(flags.snpReportPath, &report); err != nil {
return fmt.Errorf("reading snp report: %w", err)
}
snpReport := report.SNPReport
if !allEqual(snpReport.LaunchTCB, snpReport.CommittedTCB, snpReport.ReportedTCB) {
return fmt.Errorf("TCB versions are not equal: \nLaunchTCB:%+v\nCommitted TCB:%+v\nReportedTCB:%+v",
snpReport.LaunchTCB, snpReport.CommittedTCB, snpReport.ReportedTCB)
}
inputVersion := convertTCBVersionToAzureVersion(snpReport.LaunchTCB)
log.Infof("Input report: %+v", inputVersion)
client, clientClose, err := attestationconfigapi.NewClient(ctx, cfg,
[]byte(cosignPwd), []byte(privateKey), false, flags.cacheWindowSize, log)
defer func() { defer func() {
err := clientClose(cmd.Context()) err := clientClose(cmd.Context())
if err != nil { if err != nil {
@ -133,51 +135,20 @@ func runCmd(cmd *cobra.Command, _ []string) (retErr error) {
return fmt.Errorf("creating client: %w", err) return fmt.Errorf("creating client: %w", err)
} }
latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(flags.url, flags.cosignPublicKey).FetchAzureSEVSNPVersionLatest(ctx) switch uploadCfg.provider {
if err != nil { case cloudprovider.AWS:
if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) { return uploadAWS(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
log.Infof("No versions found in API, but assuming that we are uploading the first version.") case cloudprovider.Azure:
} else { return uploadAzure(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
return fmt.Errorf("fetching latest version: %w", err) default:
} return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider)
}
latestAPIVersion := latestAPIVersionAPI.AzureSEVSNPVersion
if err := client.UploadAzureSEVSNPVersionLatest(ctx, inputVersion, latestAPIVersion, flags.uploadDate, flags.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 allEqual(args ...verify.TCBVersion) bool {
if len(args) < 2 {
return true
}
firstArg := args[0]
for _, arg := range args[1:] {
if arg != firstArg {
return false
}
}
return true
}
func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.AzureSEVSNPVersion {
return attestationconfigapi.AzureSEVSNPVersion{
Bootloader: tcb.Bootloader,
TEE: tcb.TEE,
SNP: tcb.SNP,
Microcode: tcb.Microcode,
} }
} }
type config struct { type uploadConfig struct {
snpReportPath string provider cloudprovider.Provider
kind objectKind
path string
uploadDate time.Time uploadDate time.Time
cosignPublicKey string cosignPublicKey string
region string region string
@ -188,51 +159,53 @@ type config struct {
cacheWindowSize int cacheWindowSize int
} }
func parseCliFlags(cmd *cobra.Command) (config, error) { func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) {
snpReportFilePath, err := cmd.Flags().GetString("snp-report-path")
if err != nil {
return config{}, fmt.Errorf("getting maa claims path: %w", err)
}
dateStr, err := cmd.Flags().GetString("upload-date") dateStr, err := cmd.Flags().GetString("upload-date")
if err != nil { if err != nil {
return config{}, fmt.Errorf("getting upload date: %w", err) return uploadConfig{}, fmt.Errorf("getting upload date: %w", err)
} }
uploadDate := time.Now() uploadDate := time.Now()
if dateStr != "" { if dateStr != "" {
uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr) uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr)
if err != nil { if err != nil {
return config{}, fmt.Errorf("parsing date: %w", err) return uploadConfig{}, fmt.Errorf("parsing date: %w", err)
} }
} }
region, err := cmd.Flags().GetString("region") region, err := cmd.Flags().GetString("region")
if err != nil { if err != nil {
return config{}, fmt.Errorf("getting region: %w", err) return uploadConfig{}, fmt.Errorf("getting region: %w", err)
} }
bucket, err := cmd.Flags().GetString("bucket") bucket, err := cmd.Flags().GetString("bucket")
if err != nil { if err != nil {
return config{}, fmt.Errorf("getting bucket: %w", err) return uploadConfig{}, fmt.Errorf("getting bucket: %w", err)
} }
testing, err := cmd.Flags().GetBool("testing") testing, err := cmd.Flags().GetBool("testing")
if err != nil { if err != nil {
return config{}, fmt.Errorf("getting testing flag: %w", err) return uploadConfig{}, fmt.Errorf("getting testing flag: %w", err)
} }
apiCfg := getAPIEnvironment(testing) apiCfg := getAPIEnvironment(testing)
force, err := cmd.Flags().GetBool("force") force, err := cmd.Flags().GetBool("force")
if err != nil { if err != nil {
return config{}, fmt.Errorf("getting force: %w", err) return uploadConfig{}, fmt.Errorf("getting force: %w", err)
} }
cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size") cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size")
if err != nil { if err != nil {
return config{}, fmt.Errorf("getting cache window size: %w", err) return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err)
} }
return config{
snpReportPath: snpReportFilePath, provider := cloudprovider.FromString(args[0])
kind := kindFromString(args[1])
path := args[2]
return uploadConfig{
provider: provider,
kind: kind,
path: path,
uploadDate: uploadDate, uploadDate: uploadDate,
cosignPublicKey: apiCfg.cosignPublicKey, cosignPublicKey: apiCfg.cosignPublicKey,
region: region, region: region,
@ -256,9 +229,3 @@ func getAPIEnvironment(testing bool) apiConfig {
} }
return apiConfig{url: constants.CDNRepositoryURL, distribution: constants.CDNDefaultDistributionID, cosignPublicKey: constants.CosignPublicKeyReleases} return apiConfig{url: constants.CDNRepositoryURL, distribution: constants.CDNDefaultDistributionID, cosignPublicKey: constants.CosignPublicKeyReleases}
} }
func must(err error) {
if err != nil {
panic(err)
}
}

View File

@ -4,29 +4,3 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package main package main
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/verify"
"github.com/stretchr/testify/assert"
)
func TestAllEqual(t *testing.T) {
// Test case 1: One input arg
assert.True(t, allEqual(verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4}), "Expected allEqual to return true for one input arg, but got false")
// Test case 2: Three input args that are equal
assert.True(t, allEqual(
verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4},
verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4},
verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4},
), "Expected allEqual to return true for three equal input args, but got false")
// Test case 3: Three input args where second and third element are different
assert.False(t, allEqual(
verify.TCBVersion{Bootloader: 2, Microcode: 2, SNP: 3, TEE: 4},
verify.TCBVersion{Bootloader: 2, Microcode: 2, SNP: 3, TEE: 4},
verify.TCBVersion{Bootloader: 2, Microcode: 3, SNP: 3, TEE: 4},
), "Expected allEqual to return false for three input args with different second and third elements, but got true")
}

View File

@ -0,0 +1,53 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
import (
"fmt"
"strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/spf13/cobra"
)
func isCloudProvider(arg int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if provider := cloudprovider.FromString(args[arg]); provider == cloudprovider.Unknown {
return fmt.Errorf("argument %s isn't a valid cloud provider", args[arg])
}
return nil
}
}
func isValidKind(arg int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if kind := kindFromString(args[arg]); kind == unknown {
return fmt.Errorf("argument %s isn't a valid kind", args[arg])
}
return nil
}
}
// objectKind encodes the available actions.
type objectKind string
const (
// unknown is the default objectKind and does nothing.
unknown objectKind = "unknown-kind"
snpReport objectKind = "snp-report"
guestFirmware objectKind = "guest-firmware"
)
func kindFromString(s string) objectKind {
lower := strings.ToLower(s)
switch objectKind(lower) {
case snpReport, guestFirmware:
return objectKind(lower)
default:
return unknown
}
}