mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-01 03:26:08 -04:00
move attestationconfigapi to public
This commit is contained in:
parent
3b416224ca
commit
9f4dd3ad21
38 changed files with 29 additions and 29 deletions
47
api/attestationconfigapi/cli/BUILD.bazel
Normal file
47
api/attestationconfigapi/cli/BUILD.bazel
Normal file
|
@ -0,0 +1,47 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
load("//bazel/sh:def.bzl", "sh_template")
|
||||
|
||||
go_binary(
|
||||
name = "cli",
|
||||
embed = [":cli_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "cli_lib",
|
||||
srcs = [
|
||||
"compare.go",
|
||||
"delete.go",
|
||||
"main.go",
|
||||
"upload.go",
|
||||
"validargs.go",
|
||||
],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//internal/api/attestationconfigapi",
|
||||
"//internal/api/attestationconfigapi/cli/client",
|
||||
"//internal/api/fetcher",
|
||||
"//internal/attestation/variant",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
"//internal/logger",
|
||||
"//internal/staticupload",
|
||||
"//internal/verify",
|
||||
"@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_google_go_tdx_guest//proto/tdx",
|
||||
"@com_github_spf13_afero//:afero",
|
||||
"@com_github_spf13_cobra//:cobra",
|
||||
],
|
||||
)
|
||||
|
||||
sh_template(
|
||||
name = "cli_e2e_test",
|
||||
data = [":cli"],
|
||||
substitutions = {
|
||||
"@@CONFIGAPI_CLI@@": "$(rootpath :cli)",
|
||||
},
|
||||
template = "e2e/test.sh.in",
|
||||
)
|
34
api/attestationconfigapi/cli/client/BUILD.bazel
Normal file
34
api/attestationconfigapi/cli/client/BUILD.bazel
Normal file
|
@ -0,0 +1,34 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
load("//bazel/go:go_test.bzl", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "client",
|
||||
srcs = [
|
||||
"client.go",
|
||||
"reporter.go",
|
||||
],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api/attestationconfigapi",
|
||||
"//internal/api/client",
|
||||
"//internal/attestation/variant",
|
||||
"//internal/sigstore",
|
||||
"//internal/staticupload",
|
||||
"@com_github_aws_aws_sdk_go//aws",
|
||||
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "client_test",
|
||||
srcs = [
|
||||
"client_test.go",
|
||||
"reporter_test.go",
|
||||
],
|
||||
embed = [":client"],
|
||||
deps = [
|
||||
"//internal/api/attestationconfigapi",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
],
|
||||
)
|
178
api/attestationconfigapi/cli/client/client.go
Normal file
178
api/attestationconfigapi/cli/client/client.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/*
|
||||
package client contains code to manage CVM versions in Constellation's CDN API.
|
||||
It is used to upload and delete "latest" versions for AMD SEV-SNP and Intel TDX.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi"
|
||||
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/staticupload"
|
||||
)
|
||||
|
||||
// VersionFormat is the format of the version name in the S3 bucket.
|
||||
const VersionFormat = "2006-01-02-15-04"
|
||||
|
||||
// Client manages (modifies) the version information for the attestation variants.
|
||||
type Client struct {
|
||||
s3Client *apiclient.Client
|
||||
s3ClientClose func(ctx context.Context) error
|
||||
bucketID string
|
||||
signer sigstore.Signer
|
||||
cacheWindowSize int
|
||||
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// New returns a new Client.
|
||||
func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte, dryRun bool, versionWindowSize int, log *slog.Logger) (*Client, apiclient.CloseFunc, error) {
|
||||
s3Client, clientClose, err := apiclient.NewClient(ctx, cfg.Region, cfg.Bucket, cfg.DistributionID, dryRun, log)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create s3 storage: %w", err)
|
||||
}
|
||||
|
||||
repo := &Client{
|
||||
s3Client: s3Client,
|
||||
s3ClientClose: clientClose,
|
||||
signer: sigstore.NewSigner(cosignPwd, privateKey),
|
||||
bucketID: cfg.Bucket,
|
||||
cacheWindowSize: versionWindowSize,
|
||||
log: log,
|
||||
}
|
||||
return repo, clientClose, nil
|
||||
}
|
||||
|
||||
// DeleteVersion deletes the given version (without .json suffix) from the API.
|
||||
func (c Client) DeleteVersion(ctx context.Context, attestation variant.Variant, versionStr string) error {
|
||||
versions, err := c.List(ctx, attestation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch version list: %w", err)
|
||||
}
|
||||
|
||||
ops, err := c.deleteVersion(versions, versionStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return executeAllCmds(ctx, c.s3Client, ops)
|
||||
}
|
||||
|
||||
// List returns the list of versions for the given attestation variant.
|
||||
func (c Client) List(ctx context.Context, attestation variant.Variant) (attestationconfigapi.List, error) {
|
||||
versions, err := apiclient.Fetch(ctx, c.s3Client, attestationconfigapi.List{Variant: attestation})
|
||||
if err != nil {
|
||||
var notFoundErr *apiclient.NotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
return attestationconfigapi.List{Variant: attestation}, nil
|
||||
}
|
||||
return attestationconfigapi.List{}, err
|
||||
}
|
||||
|
||||
versions.Variant = attestation
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (c Client) deleteVersion(versions attestationconfigapi.List, versionStr string) (ops []crudCmd, err error) {
|
||||
versionStr = versionStr + ".json"
|
||||
ops = append(ops, deleteCmd{
|
||||
apiObject: attestationconfigapi.Entry{
|
||||
Variant: versions.Variant,
|
||||
Version: versionStr,
|
||||
},
|
||||
})
|
||||
|
||||
removedVersions, err := removeVersion(versions, versionStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = append(ops, putCmd{
|
||||
apiObject: removedVersions,
|
||||
signer: c.signer,
|
||||
})
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
func (c Client) listCachedVersions(ctx context.Context, attestation variant.Variant) ([]string, error) {
|
||||
list, err := c.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(c.bucketID),
|
||||
Prefix: aws.String(reportVersionDir(attestation)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list objects: %w", err)
|
||||
}
|
||||
|
||||
var dates []string
|
||||
for _, obj := range list.Contents {
|
||||
fileName := path.Base(*obj.Key)
|
||||
|
||||
// The cache contains signature and json files
|
||||
// We only want the json files
|
||||
if date, ok := strings.CutSuffix(fileName, ".json"); ok {
|
||||
dates = append(dates, date)
|
||||
}
|
||||
}
|
||||
return dates, nil
|
||||
}
|
||||
|
||||
func removeVersion(list attestationconfigapi.List, versionStr string) (removedVersions attestationconfigapi.List, err error) {
|
||||
versions := list.List
|
||||
for i, v := range versions {
|
||||
if v == versionStr {
|
||||
if i == len(versions)-1 {
|
||||
removedVersions = attestationconfigapi.List{List: versions[:i], Variant: list.Variant}
|
||||
} else {
|
||||
removedVersions = attestationconfigapi.List{List: append(versions[:i], versions[i+1:]...), Variant: list.Variant}
|
||||
}
|
||||
return removedVersions, nil
|
||||
}
|
||||
}
|
||||
return attestationconfigapi.List{}, fmt.Errorf("version %s not found in list %v", versionStr, versions)
|
||||
}
|
||||
|
||||
type crudCmd interface {
|
||||
Execute(ctx context.Context, c *apiclient.Client) error
|
||||
}
|
||||
|
||||
type deleteCmd struct {
|
||||
apiObject apiclient.APIObject
|
||||
}
|
||||
|
||||
func (d deleteCmd) Execute(ctx context.Context, c *apiclient.Client) error {
|
||||
return apiclient.DeleteWithSignature(ctx, c, d.apiObject)
|
||||
}
|
||||
|
||||
type putCmd struct {
|
||||
apiObject apiclient.APIObject
|
||||
signer sigstore.Signer
|
||||
}
|
||||
|
||||
func (p putCmd) Execute(ctx context.Context, c *apiclient.Client) error {
|
||||
return apiclient.SignAndUpdate(ctx, c, p.apiObject, p.signer)
|
||||
}
|
||||
|
||||
func executeAllCmds(ctx context.Context, client *apiclient.Client, cmds []crudCmd) error {
|
||||
for _, cmd := range cmds {
|
||||
if err := cmd.Execute(ctx, client); err != nil {
|
||||
return fmt.Errorf("execute operation %+v: %w", cmd, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
34
api/attestationconfigapi/cli/client/client_test.go
Normal file
34
api/attestationconfigapi/cli/client/client_test.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteAzureSEVSNPVersions(t *testing.T) {
|
||||
sut := Client{
|
||||
bucketID: "bucket",
|
||||
}
|
||||
versions := attestationconfigapi.List{List: []string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}}
|
||||
|
||||
ops, err := sut.deleteVersion(versions, "2021-01-01")
|
||||
|
||||
assert := assert.New(t)
|
||||
assert.NoError(err)
|
||||
assert.Contains(ops, deleteCmd{
|
||||
apiObject: attestationconfigapi.Entry{
|
||||
Version: "2021-01-01.json",
|
||||
},
|
||||
})
|
||||
|
||||
assert.Contains(ops, putCmd{
|
||||
apiObject: attestationconfigapi.List{List: []string{"2023-01-01.json", "2019-01-01.json"}},
|
||||
})
|
||||
}
|
367
api/attestationconfigapi/cli/client/reporter.go
Normal file
367
api/attestationconfigapi/cli/client/reporter.go
Normal file
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/client"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
)
|
||||
|
||||
// cachedVersionsSubDir is the subdirectory in the bucket where the cached versions are stored.
|
||||
const cachedVersionsSubDir = "cached-versions"
|
||||
|
||||
// ErrNoNewerVersion is returned if the input version is not newer than the latest API version.
|
||||
var ErrNoNewerVersion = errors.New("input version is not newer than latest API version")
|
||||
|
||||
func reportVersionDir(attestation variant.Variant) string {
|
||||
return path.Join(attestationconfigapi.AttestationURLPath, attestation.String(), cachedVersionsSubDir)
|
||||
}
|
||||
|
||||
// IsInputNewerThanOtherVersion compares the input version with the other version and returns true if the input version is newer.
|
||||
// This function panics if the input versions are not TDX or SEV-SNP versions.
|
||||
func IsInputNewerThanOtherVersion(variant variant.Variant, inputVersion, otherVersion any) bool {
|
||||
var result bool
|
||||
actionForVariant(variant,
|
||||
func() {
|
||||
input := inputVersion.(attestationconfigapi.TDXVersion)
|
||||
other := otherVersion.(attestationconfigapi.TDXVersion)
|
||||
result = isInputNewerThanOtherTDXVersion(input, other)
|
||||
},
|
||||
func() {
|
||||
input := inputVersion.(attestationconfigapi.SEVSNPVersion)
|
||||
other := otherVersion.(attestationconfigapi.SEVSNPVersion)
|
||||
result = isInputNewerThanOtherSEVSNPVersion(input, other)
|
||||
},
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
// UploadLatestVersion saves the given version to the cache, determines the smallest
|
||||
// TCB version in the cache among the last cacheWindowSize versions and updates
|
||||
// the latest version in the API if there is an update.
|
||||
// force can be used to bypass the validation logic against the cached versions.
|
||||
func (c Client) UploadLatestVersion(
|
||||
ctx context.Context, attestationVariant variant.Variant,
|
||||
inputVersion, latestVersionInAPI any,
|
||||
now time.Time, force bool,
|
||||
) error {
|
||||
// Validate input versions against configured attestation variant
|
||||
// This allows us to skip these checks in the individual variant implementations
|
||||
var err error
|
||||
actionForVariant(attestationVariant,
|
||||
func() {
|
||||
if _, ok := inputVersion.(attestationconfigapi.TDXVersion); !ok {
|
||||
err = fmt.Errorf("input version %q is not a TDX version", inputVersion)
|
||||
}
|
||||
if _, ok := latestVersionInAPI.(attestationconfigapi.TDXVersion); !ok {
|
||||
err = fmt.Errorf("latest API version %q is not a TDX version", latestVersionInAPI)
|
||||
}
|
||||
},
|
||||
func() {
|
||||
if _, ok := inputVersion.(attestationconfigapi.SEVSNPVersion); !ok {
|
||||
err = fmt.Errorf("input version %q is not a SNP version", inputVersion)
|
||||
}
|
||||
if _, ok := latestVersionInAPI.(attestationconfigapi.SEVSNPVersion); !ok {
|
||||
err = fmt.Errorf("latest API version %q is not a SNP version", latestVersionInAPI)
|
||||
}
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.addVersionToCache(ctx, attestationVariant, inputVersion, now); err != nil {
|
||||
return fmt.Errorf("adding version to cache: %w", err)
|
||||
}
|
||||
|
||||
// If force is set, immediately update the latest version to the new version in the API.
|
||||
if force {
|
||||
return c.uploadAsLatestVersion(ctx, attestationVariant, inputVersion, now)
|
||||
}
|
||||
|
||||
// Otherwise, check the cached versions and update the latest version in the API if necessary.
|
||||
versionDates, err := c.listCachedVersions(ctx, attestationVariant)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing existing cached versions: %w", err)
|
||||
}
|
||||
if len(versionDates) < c.cacheWindowSize {
|
||||
c.log.Warn(fmt.Sprintf("Skipping version update, found %d, expected %d reported versions.", len(versionDates), c.cacheWindowSize))
|
||||
return nil
|
||||
}
|
||||
|
||||
minVersion, minDate, err := c.findMinVersion(ctx, attestationVariant, versionDates)
|
||||
if err != nil {
|
||||
return fmt.Errorf("determining minimal version in cache: %w", err)
|
||||
}
|
||||
c.log.Info(fmt.Sprintf("Found minimal version: %+v with date: %s", minVersion, minDate))
|
||||
|
||||
if !IsInputNewerThanOtherVersion(attestationVariant, minVersion, latestVersionInAPI) {
|
||||
c.log.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v. Skipping list update", minVersion, latestVersionInAPI))
|
||||
return ErrNoNewerVersion
|
||||
}
|
||||
|
||||
c.log.Info(fmt.Sprintf("Input version: %+v is newer than latest API version: %+v", minVersion, latestVersionInAPI))
|
||||
t, err := time.Parse(VersionFormat, minDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing date: %w", err)
|
||||
}
|
||||
|
||||
if err := c.uploadAsLatestVersion(ctx, attestationVariant, minVersion, t); err != nil {
|
||||
return fmt.Errorf("uploading as latest version: %w", err)
|
||||
}
|
||||
|
||||
c.log.Info(fmt.Sprintf("Successfully uploaded new %s version: %+v", attestationVariant, minVersion))
|
||||
return nil
|
||||
}
|
||||
|
||||
// uploadAsLatestVersion uploads the given version and updates the list to set it as the "latest" version.
|
||||
// The version's name is the UTC timestamp of the date.
|
||||
// The /list entry stores the version name + .json suffix.
|
||||
func (c Client) uploadAsLatestVersion(ctx context.Context, variant variant.Variant, inputVersion any, date time.Time) error {
|
||||
versions, err := c.List(ctx, variant)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch version list: %w", err)
|
||||
}
|
||||
if !variant.Equal(versions.Variant) {
|
||||
return nil
|
||||
}
|
||||
|
||||
dateStr := date.Format(VersionFormat) + ".json"
|
||||
var ops []crudCmd
|
||||
|
||||
obj := apiVersionObject{version: dateStr, variant: variant, cached: false}
|
||||
obj.setVersion(inputVersion)
|
||||
ops = append(ops, putCmd{
|
||||
apiObject: obj,
|
||||
signer: c.signer,
|
||||
})
|
||||
|
||||
versions.AddVersion(dateStr)
|
||||
|
||||
ops = append(ops, putCmd{
|
||||
apiObject: versions,
|
||||
signer: c.signer,
|
||||
})
|
||||
|
||||
return executeAllCmds(ctx, c.s3Client, ops)
|
||||
}
|
||||
|
||||
// addVersionToCache adds the given version to the cache.
|
||||
func (c Client) addVersionToCache(ctx context.Context, variant variant.Variant, inputVersion any, date time.Time) error {
|
||||
dateStr := date.Format(VersionFormat) + ".json"
|
||||
obj := apiVersionObject{version: dateStr, variant: variant, cached: true}
|
||||
obj.setVersion(inputVersion)
|
||||
cmd := putCmd{
|
||||
apiObject: obj,
|
||||
signer: c.signer,
|
||||
}
|
||||
return cmd.Execute(ctx, c.s3Client)
|
||||
}
|
||||
|
||||
// findMinVersion returns the minimal version in the cache among the last cacheWindowSize versions.
|
||||
func (c Client) findMinVersion(
|
||||
ctx context.Context, attestationVariant variant.Variant, versionDates []string,
|
||||
) (any, string, error) {
|
||||
var getMinimalVersion func() (any, string, error)
|
||||
|
||||
actionForVariant(attestationVariant,
|
||||
func() {
|
||||
getMinimalVersion = func() (any, string, error) {
|
||||
return findMinimalVersion[attestationconfigapi.TDXVersion](ctx, attestationVariant, versionDates, c.s3Client, c.cacheWindowSize)
|
||||
}
|
||||
},
|
||||
func() {
|
||||
getMinimalVersion = func() (any, string, error) {
|
||||
return findMinimalVersion[attestationconfigapi.SEVSNPVersion](ctx, attestationVariant, versionDates, c.s3Client, c.cacheWindowSize)
|
||||
}
|
||||
},
|
||||
)
|
||||
return getMinimalVersion()
|
||||
}
|
||||
|
||||
func findMinimalVersion[T attestationconfigapi.TDXVersion | attestationconfigapi.SEVSNPVersion](
|
||||
ctx context.Context, variant variant.Variant, versionDates []string,
|
||||
s3Client *client.Client, cacheWindowSize int,
|
||||
) (T, string, error) {
|
||||
var minimalVersion *T
|
||||
var minimalDate string
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions
|
||||
versionDates = versionDates[:cacheWindowSize]
|
||||
sort.Strings(versionDates) // sort with oldest first to to take the minimal version with the oldest date
|
||||
|
||||
for _, date := range versionDates {
|
||||
obj, err := client.Fetch(ctx, s3Client, apiVersionObject{version: date + ".json", variant: variant, cached: true})
|
||||
if err != nil {
|
||||
return *new(T), "", fmt.Errorf("get object: %w", err)
|
||||
}
|
||||
obj.variant = variant // variant is not set by Fetch, set it manually
|
||||
|
||||
if minimalVersion == nil {
|
||||
v := obj.getVersion().(T)
|
||||
minimalVersion = &v
|
||||
minimalDate = date
|
||||
continue
|
||||
}
|
||||
|
||||
// If the current minimal version has newer versions than the one we just fetched,
|
||||
// update the minimal version to the older version.
|
||||
if IsInputNewerThanOtherVersion(variant, *minimalVersion, obj.getVersion()) {
|
||||
v := obj.getVersion().(T)
|
||||
minimalVersion = &v
|
||||
minimalDate = date
|
||||
}
|
||||
}
|
||||
|
||||
return *minimalVersion, minimalDate, nil
|
||||
}
|
||||
|
||||
type apiVersionObject struct {
|
||||
version string `json:"-"`
|
||||
variant variant.Variant `json:"-"`
|
||||
cached bool `json:"-"`
|
||||
snp attestationconfigapi.SEVSNPVersion
|
||||
tdx attestationconfigapi.TDXVersion
|
||||
}
|
||||
|
||||
func (a apiVersionObject) MarshalJSON() ([]byte, error) {
|
||||
var res []byte
|
||||
var err error
|
||||
actionForVariant(a.variant,
|
||||
func() {
|
||||
res, err = json.Marshal(a.tdx)
|
||||
},
|
||||
func() {
|
||||
res, err = json.Marshal(a.snp)
|
||||
},
|
||||
)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (a *apiVersionObject) UnmarshalJSON(data []byte) error {
|
||||
errTDX := json.Unmarshal(data, &a.tdx)
|
||||
errSNP := json.Unmarshal(data, &a.snp)
|
||||
if errTDX == nil || errSNP == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("trying to unmarshal data into both TDX and SNP versions: %w", errors.Join(errTDX, errSNP))
|
||||
}
|
||||
|
||||
// JSONPath returns the path to the JSON file for the request to the config api.
|
||||
// This is the path to the cached version in the S3 bucket.
|
||||
func (a apiVersionObject) JSONPath() string {
|
||||
if a.cached {
|
||||
return path.Join(reportVersionDir(a.variant), a.version)
|
||||
}
|
||||
return path.Join(attestationconfigapi.AttestationURLPath, a.variant.String(), a.version)
|
||||
}
|
||||
|
||||
// ValidateRequest validates the request.
|
||||
func (a apiVersionObject) ValidateRequest() error {
|
||||
if !strings.HasSuffix(a.version, ".json") {
|
||||
return fmt.Errorf("version has no .json suffix")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate is a No-Op.
|
||||
func (a apiVersionObject) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVersion returns the version.
|
||||
func (a apiVersionObject) getVersion() any {
|
||||
var res any
|
||||
actionForVariant(a.variant,
|
||||
func() {
|
||||
res = a.tdx
|
||||
},
|
||||
func() {
|
||||
res = a.snp
|
||||
},
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
// setVersion sets the version.
|
||||
func (a *apiVersionObject) setVersion(version any) {
|
||||
actionForVariant(a.variant,
|
||||
func() {
|
||||
a.tdx = version.(attestationconfigapi.TDXVersion)
|
||||
},
|
||||
func() {
|
||||
a.snp = version.(attestationconfigapi.SEVSNPVersion)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// actionForVariant performs the given action based on the whether variant is a TDX or SEV-SNP variant.
|
||||
func actionForVariant(
|
||||
attestationVariant variant.Variant,
|
||||
tdxAction func(), snpAction func(),
|
||||
) {
|
||||
switch attestationVariant {
|
||||
case variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.GCPSEVSNP{}:
|
||||
snpAction()
|
||||
case variant.AzureTDX{}:
|
||||
tdxAction()
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported attestation variant: %s", attestationVariant))
|
||||
}
|
||||
}
|
||||
|
||||
// isInputNewerThanOtherSEVSNPVersion compares all version fields and returns false if any input field is older, or the versions are equal.
|
||||
func isInputNewerThanOtherSEVSNPVersion(input, other attestationconfigapi.SEVSNPVersion) bool {
|
||||
if input == other {
|
||||
return false
|
||||
}
|
||||
if input.TEE < other.TEE {
|
||||
return false
|
||||
}
|
||||
if input.SNP < other.SNP {
|
||||
return false
|
||||
}
|
||||
if input.Microcode < other.Microcode {
|
||||
return false
|
||||
}
|
||||
if input.Bootloader < other.Bootloader {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isInputNewerThanOtherSEVSNPVersion compares all version fields and returns false if any input field is older, or the versions are equal.
|
||||
func isInputNewerThanOtherTDXVersion(input, other attestationconfigapi.TDXVersion) bool {
|
||||
if input == other {
|
||||
return false
|
||||
}
|
||||
|
||||
if input.PCESVN < other.PCESVN {
|
||||
return false
|
||||
}
|
||||
if input.QESVN < other.QESVN {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate component-wise security version numbers
|
||||
for idx, inputVersion := range input.TEETCBSVN {
|
||||
if inputVersion < other.TEETCBSVN[idx] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
138
api/attestationconfigapi/cli/client/reporter_test.go
Normal file
138
api/attestationconfigapi/cli/client/reporter_test.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsInputNewerThanOtherSEVSNPVersion(t *testing.T) {
|
||||
newTestCfg := func() attestationconfigapi.SEVSNPVersion {
|
||||
return attestationconfigapi.SEVSNPVersion{
|
||||
Microcode: 93,
|
||||
TEE: 0,
|
||||
SNP: 6,
|
||||
Bootloader: 2,
|
||||
}
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
latest attestationconfigapi.SEVSNPVersion
|
||||
input attestationconfigapi.SEVSNPVersion
|
||||
expect bool
|
||||
}{
|
||||
"input is older than latest": {
|
||||
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||
c.Microcode--
|
||||
return c
|
||||
}(newTestCfg()),
|
||||
latest: newTestCfg(),
|
||||
expect: false,
|
||||
},
|
||||
"input has greater and smaller version field than latest": {
|
||||
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||
c.Microcode++
|
||||
c.Bootloader--
|
||||
return c
|
||||
}(newTestCfg()),
|
||||
latest: newTestCfg(),
|
||||
expect: false,
|
||||
},
|
||||
"input is newer than latest": {
|
||||
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||
c.TEE++
|
||||
return c
|
||||
}(newTestCfg()),
|
||||
latest: newTestCfg(),
|
||||
expect: true,
|
||||
},
|
||||
"input is equal to latest": {
|
||||
input: newTestCfg(),
|
||||
latest: newTestCfg(),
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
isNewer := isInputNewerThanOtherSEVSNPVersion(tc.input, tc.latest)
|
||||
assert.Equal(t, tc.expect, isNewer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInputNewerThanOtherTDXVersion(t *testing.T) {
|
||||
newTestVersion := func() attestationconfigapi.TDXVersion {
|
||||
return attestationconfigapi.TDXVersion{
|
||||
QESVN: 1,
|
||||
PCESVN: 2,
|
||||
TEETCBSVN: [16]byte{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
|
||||
QEVendorID: [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
|
||||
XFAM: [8]byte{0, 1, 2, 3, 4, 5, 6, 7},
|
||||
}
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
latest attestationconfigapi.TDXVersion
|
||||
input attestationconfigapi.TDXVersion
|
||||
expect bool
|
||||
}{
|
||||
"input is older than latest": {
|
||||
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||
c.QESVN--
|
||||
return c
|
||||
}(newTestVersion()),
|
||||
latest: newTestVersion(),
|
||||
expect: false,
|
||||
},
|
||||
"input has greater and smaller version field than latest": {
|
||||
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||
c.QESVN++
|
||||
c.PCESVN--
|
||||
return c
|
||||
}(newTestVersion()),
|
||||
latest: newTestVersion(),
|
||||
expect: false,
|
||||
},
|
||||
"input is newer than latest": {
|
||||
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||
c.QESVN++
|
||||
return c
|
||||
}(newTestVersion()),
|
||||
latest: newTestVersion(),
|
||||
expect: true,
|
||||
},
|
||||
"input is equal to latest": {
|
||||
input: newTestVersion(),
|
||||
latest: newTestVersion(),
|
||||
expect: false,
|
||||
},
|
||||
"tee tcb svn is newer": {
|
||||
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||
c.TEETCBSVN[4]++
|
||||
return c
|
||||
}(newTestVersion()),
|
||||
latest: newTestVersion(),
|
||||
expect: true,
|
||||
},
|
||||
"xfam is different": {
|
||||
input: func(c attestationconfigapi.TDXVersion) attestationconfigapi.TDXVersion {
|
||||
c.XFAM[3]++
|
||||
return c
|
||||
}(newTestVersion()),
|
||||
latest: newTestVersion(),
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
isNewer := isInputNewerThanOtherTDXVersion(tc.input, tc.latest)
|
||||
assert.Equal(t, tc.expect, isNewer)
|
||||
})
|
||||
}
|
||||
}
|
101
api/attestationconfigapi/cli/compare.go
Normal file
101
api/attestationconfigapi/cli/compare.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi/cli/client"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/verify"
|
||||
"github.com/google/go-tdx-guest/proto/tdx"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newCompareCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "compare VARIANT FILE [FILE...]",
|
||||
Short: "Returns the minimum version of all given attestation reports.",
|
||||
Long: "Compare a list of attestation reports and return the report with the minimum version.",
|
||||
Example: "cli compare azure-sev-snp report1.json report2.json",
|
||||
Args: cobra.MatchAll(cobra.MinimumNArgs(2), arg0isAttestationVariant()),
|
||||
RunE: runCompare,
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCompare(cmd *cobra.Command, args []string) error {
|
||||
cmd.SetOut(os.Stdout)
|
||||
|
||||
variant, err := variant.FromString(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing variant: %w", err)
|
||||
}
|
||||
|
||||
return compare(cmd, variant, args[1:], file.NewHandler(afero.NewOsFs()))
|
||||
}
|
||||
|
||||
func compare(cmd *cobra.Command, attestationVariant variant.Variant, files []string, fs file.Handler) (retErr error) {
|
||||
if !slices.Contains([]variant.Variant{variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.GCPSEVSNP{}, variant.AzureTDX{}}, attestationVariant) {
|
||||
return fmt.Errorf("variant %s not supported", attestationVariant)
|
||||
}
|
||||
|
||||
lowestVersion, err := compareVersions(attestationVariant, files, fs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("comparing versions: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println(lowestVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareVersions(attestationVariant variant.Variant, files []string, fs file.Handler) (string, error) {
|
||||
readReport := readSNPReport
|
||||
if attestationVariant.Equal(variant.AzureTDX{}) {
|
||||
readReport = readTDXReport
|
||||
}
|
||||
|
||||
lowestVersion := files[0]
|
||||
lowestReport, err := readReport(files[0], fs)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading report: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files[1:] {
|
||||
report, err := readReport(file, fs)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading report: %w", err)
|
||||
}
|
||||
|
||||
if client.IsInputNewerThanOtherVersion(attestationVariant, lowestReport, report) {
|
||||
lowestVersion = file
|
||||
lowestReport = report
|
||||
}
|
||||
}
|
||||
|
||||
return lowestVersion, nil
|
||||
}
|
||||
|
||||
func readSNPReport(file string, fs file.Handler) (any, error) {
|
||||
var report verify.Report
|
||||
if err := fs.ReadJSON(file, &report); err != nil {
|
||||
return nil, fmt.Errorf("reading snp report: %w", err)
|
||||
}
|
||||
return convertTCBVersionToSNPVersion(report.SNPReport.LaunchTCB), nil
|
||||
}
|
||||
|
||||
func readTDXReport(file string, fs file.Handler) (any, error) {
|
||||
var report *tdx.QuoteV4
|
||||
if err := fs.ReadJSON(file, &report); err != nil {
|
||||
return nil, fmt.Errorf("reading tdx report: %w", err)
|
||||
}
|
||||
return convertQuoteToTDXVersion(report), nil
|
||||
}
|
196
api/attestationconfigapi/cli/delete.go
Normal file
196
api/attestationconfigapi/cli/delete.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
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/api/attestationconfigapi"
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi/cli/client"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/edgelesssys/constellation/v2/internal/staticupload"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newDeleteCmd creates the delete command.
|
||||
func newDeleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete VARIANT KIND <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-sev-snp attestation-report 1.0.0",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(3), arg0isAttestationVariant(), isValidKind(1)),
|
||||
PreRunE: envCheck,
|
||||
RunE: runDelete,
|
||||
}
|
||||
|
||||
recursivelyCmd := &cobra.Command{
|
||||
Use: "recursive {aws-sev-snp|azure-sev-snp|azure-tdx|gcp-sev-snp}",
|
||||
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-sev-snp",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), arg0isAttestationVariant()),
|
||||
RunE: runRecursiveDelete,
|
||||
}
|
||||
|
||||
cmd.AddCommand(recursivelyCmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDelete(cmd *cobra.Command, args []string) (retErr error) {
|
||||
log := logger.NewTextLogger(slog.LevelDebug).WithGroup("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 := client.New(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))
|
||||
}
|
||||
}()
|
||||
|
||||
return deleteEntry(cmd.Context(), client, deleteCfg)
|
||||
}
|
||||
|
||||
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.NewTextLogger(slog.LevelDebug).WithGroup("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))
|
||||
}
|
||||
}()
|
||||
|
||||
deletePath := path.Join(attestationconfigapi.AttestationURLPath, deleteCfg.variant.String())
|
||||
|
||||
return deleteEntryRecursive(cmd.Context(), deletePath, client, deleteCfg)
|
||||
}
|
||||
|
||||
type deleteConfig struct {
|
||||
variant variant.Variant
|
||||
kind objectKind
|
||||
version string
|
||||
region string
|
||||
bucket string
|
||||
url string
|
||||
distribution string
|
||||
cosignPublicKey string
|
||||
}
|
||||
|
||||
func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) {
|
||||
region, err := cmd.Flags().GetString("region")
|
||||
if err != nil {
|
||||
return deleteConfig{}, fmt.Errorf("getting region: %w", err)
|
||||
}
|
||||
|
||||
bucket, err := cmd.Flags().GetString("bucket")
|
||||
if err != nil {
|
||||
return deleteConfig{}, fmt.Errorf("getting bucket: %w", err)
|
||||
}
|
||||
|
||||
testing, err := cmd.Flags().GetBool("testing")
|
||||
if err != nil {
|
||||
return deleteConfig{}, fmt.Errorf("getting testing flag: %w", err)
|
||||
}
|
||||
apiCfg := getAPIEnvironment(testing)
|
||||
|
||||
variant, err := variant.FromString(args[0])
|
||||
if err != nil {
|
||||
return deleteConfig{}, fmt.Errorf("invalid attestation variant: %q: %w", args[0], err)
|
||||
}
|
||||
kind := kindFromString(args[1])
|
||||
version := args[2]
|
||||
|
||||
return deleteConfig{
|
||||
variant: variant,
|
||||
kind: kind,
|
||||
version: version,
|
||||
region: region,
|
||||
bucket: bucket,
|
||||
url: apiCfg.url,
|
||||
distribution: apiCfg.distribution,
|
||||
cosignPublicKey: apiCfg.cosignPublicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func deleteEntry(ctx context.Context, client *client.Client, cfg deleteConfig) error {
|
||||
if cfg.kind != attestationReport {
|
||||
return fmt.Errorf("kind %s not supported", cfg.kind)
|
||||
}
|
||||
|
||||
return client.DeleteVersion(ctx, cfg.variant, 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
|
||||
}
|
244
api/attestationconfigapi/cli/e2e/test.sh.in
Executable file
244
api/attestationconfigapi/cli/e2e/test.sh.in
Executable file
|
@ -0,0 +1,244 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Try to upload a file to S3 and then delete it using the configapi cli.
|
||||
# Check the file exists after uploading it.
|
||||
# Check the file does not exist after deleting it.
|
||||
|
||||
###### script header ######
|
||||
|
||||
lib=$(realpath @@BASE_LIB@@) || exit 1
|
||||
stat "${lib}" >> /dev/null || exit 1
|
||||
|
||||
# shellcheck source=../../../../../bazel/sh/lib.bash
|
||||
if ! source "${lib}"; then
|
||||
echo "Error: could not find import"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
configapi_cli=$(realpath @@CONFIGAPI_CLI@@)
|
||||
stat "${configapi_cli}" >> /dev/null
|
||||
configapi_cli="${configapi_cli} --testing"
|
||||
###### script body ######
|
||||
attestationVariant=$1
|
||||
readonly attestationVariant
|
||||
|
||||
readonly region="eu-west-1"
|
||||
readonly bucket="resource-api-testing"
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
readonly tmpdir
|
||||
registerExitHandler "rm -rf ${tmpdir}"
|
||||
|
||||
# empty the bucket version state
|
||||
${configapi_cli} delete recursive "${attestationVariant}" --region "${region}" --bucket "${bucket}"
|
||||
|
||||
readonly current_report_path="${tmpdir}/attestationReportCurrent.json"
|
||||
readonly report_path="${tmpdir}/attestationReport.json"
|
||||
readonly older_report_path="${tmpdir}/attestationReportOld.json"
|
||||
|
||||
if [[ ${attestationVariant} == *-tdx ]]; then
|
||||
cat << EOF > "${current_report_path}"
|
||||
{
|
||||
"header": {
|
||||
"qe_svn": "AAA=",
|
||||
"pce_svn": "AAA=",
|
||||
"qe_vendor_id": "KioqKioqKioqKioqKioqKg=="
|
||||
},
|
||||
"td_quote_body": {
|
||||
"tee_tcb_svn": "AAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"xfam": "AAAAAAAAAAA="
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# the high version numbers ensure that it's newer than the current latest value
|
||||
cat << EOF > "${report_path}"
|
||||
{
|
||||
"header": {
|
||||
"qe_svn": "//8=",
|
||||
"pce_svn": "//8=",
|
||||
"qe_vendor_id": "KioqKioqKioqKioqKioqKg=="
|
||||
},
|
||||
"td_quote_body": {
|
||||
"tee_tcb_svn": "/////////////////////w==",
|
||||
"xfam": "AQIDBAUGBwg="
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# has an older version
|
||||
cat << EOF > "${older_report_path}"
|
||||
{
|
||||
"header": {
|
||||
"qe_svn": "//8=",
|
||||
"pce_svn": "/v8=",
|
||||
"qe_vendor_id": "KioqKioqKioqKioqKioqKg=="
|
||||
},
|
||||
"td_quote_body": {
|
||||
"tee_tcb_svn": "/////////////////////g==",
|
||||
"xfam": "AQIDBAUGBwg="
|
||||
}
|
||||
}
|
||||
EOF
|
||||
elif [[ ${attestationVariant} == *-sev-snp ]]; then
|
||||
cat << EOF > "${current_report_path}"
|
||||
{
|
||||
"snp_report": {
|
||||
"reported_tcb": {
|
||||
"bootloader": 1,
|
||||
"tee": 1,
|
||||
"snp": 1,
|
||||
"microcode": 1
|
||||
},
|
||||
"committed_tcb": {
|
||||
"bootloader": 1,
|
||||
"tee": 1,
|
||||
"snp": 1,
|
||||
"microcode": 1
|
||||
},
|
||||
"launch_tcb": {
|
||||
"bootloader": 1,
|
||||
"tee": 1,
|
||||
"snp": 1,
|
||||
"microcode": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# the high version numbers ensure that it's newer than the current latest value
|
||||
cat << EOF > "${report_path}"
|
||||
{
|
||||
"snp_report": {
|
||||
"reported_tcb": {
|
||||
"bootloader": 255,
|
||||
"tee": 255,
|
||||
"snp": 255,
|
||||
"microcode": 255
|
||||
},
|
||||
"committed_tcb": {
|
||||
"bootloader": 255,
|
||||
"tee": 255,
|
||||
"snp": 255,
|
||||
"microcode": 255
|
||||
},
|
||||
"launch_tcb": {
|
||||
"bootloader": 255,
|
||||
"tee": 255,
|
||||
"snp": 255,
|
||||
"microcode": 255
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# has an older version
|
||||
cat << EOF > "${older_report_path}"
|
||||
{
|
||||
"snp_report": {
|
||||
"reported_tcb": {
|
||||
"bootloader": 255,
|
||||
"tee": 255,
|
||||
"snp": 255,
|
||||
"microcode": 254
|
||||
},
|
||||
"committed_tcb": {
|
||||
"bootloader": 255,
|
||||
"tee": 255,
|
||||
"snp": 255,
|
||||
"microcode": 254
|
||||
},
|
||||
"launch_tcb": {
|
||||
"bootloader": 255,
|
||||
"tee": 255,
|
||||
"snp": 255,
|
||||
"microcode": 254
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo "Unknown attestation variant: ${attestationVariant}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# upload a fake latest version for the fetcher
|
||||
${configapi_cli} upload "${attestationVariant}" attestation-report "${current_report_path}" --force --upload-date "2000-01-01-01-01" --region "${region}" --bucket "${bucket}"
|
||||
|
||||
# report 3 versions with different dates to fill the reporter cache
|
||||
readonly date_oldest="2023-02-01-03-04"
|
||||
${configapi_cli} upload "${attestationVariant}" attestation-report "${older_report_path}" --upload-date "${date_oldest}" --region "${region}" --bucket "${bucket}" --cache-window-size 3
|
||||
readonly date_older="2023-02-02-03-04"
|
||||
${configapi_cli} upload "${attestationVariant}" attestation-report "${older_report_path}" --upload-date "${date_older}" --region "${region}" --bucket "${bucket}" --cache-window-size 3
|
||||
readonly date="2023-02-03-03-04"
|
||||
${configapi_cli} upload "${attestationVariant}" attestation-report "${report_path}" --upload-date "${date}" --region "${region}" --bucket "${bucket}" --cache-window-size 3
|
||||
|
||||
# expect that $date_oldest is served as latest version
|
||||
basepath="constellation/v1/attestation/${attestationVariant}"
|
||||
baseurl="https://d33dzgxuwsgbpw.cloudfront.net/${basepath}"
|
||||
if ! curl -fsSL "${baseurl}/${date_oldest}.json" > version.json; then
|
||||
echo "Checking for uploaded version file ${basepath}/${date_oldest}.json: request returned ${?}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ${attestationVariant} == *-tdx ]]; then
|
||||
# check that version values are equal to expected
|
||||
if ! cmp -s <(echo -n '{"qeSVN":65535,"pceSVN":65534,"teeTCBSVN":[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254],"qeVendorID":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42],"xfam":[1,2,3,4,5,6,7,8]}') version.json; then
|
||||
echo "The version content:"
|
||||
cat version.json
|
||||
echo " is not equal to the expected version content:"
|
||||
echo '{"qeSVN":65535,"pceSVN":65534,"teeTCBSVN":[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254],"qeVendorID":[42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42],"xfam":[1,2,3,4,5,6,7,8]}'
|
||||
exit 1
|
||||
fi
|
||||
elif [[ ${attestationVariant} == *-sev-snp ]]; then
|
||||
# check that version values are equal to expected
|
||||
if ! cmp -s <(echo -n '{"bootloader":255,"tee":255,"snp":255,"microcode":254}') version.json; then
|
||||
echo "The version content:"
|
||||
cat version.json
|
||||
echo " is not equal to the expected version content:"
|
||||
echo '{"bootloader":255,"tee":255,"snp":255,"microcode":254}'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! curl -fsSL "${baseurl}/${date_oldest}.json.sig" > /dev/null; then
|
||||
echo "Checking for uploaded version signature file ${basepath}/${date_oldest}.json.sig: request returned ${?}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check list endpoint
|
||||
if ! curl -fsSL "${baseurl}"/list > list.json; then
|
||||
echo "Checking for uploaded list file ${basepath}/list: request returned ${?}"
|
||||
exit 1
|
||||
fi
|
||||
# check that version values are equal to expected
|
||||
if ! cmp -s <(echo -n '["2023-02-01-03-04.json","2000-01-01-01-01.json"]') list.json; then
|
||||
echo "The list content:"
|
||||
cat list.json
|
||||
echo " is not equal to the expected version content:"
|
||||
echo '["2023-02-01-03-04.json","2000-01-01-01-01.json"]'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check that the other versions are not uploaded
|
||||
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}/${date_older}.json")
|
||||
if [[ ${http_code} -ne 404 ]]; then
|
||||
echo "Expected HTTP code 404 for: ${basepath}/${date_older}.json, but got ${http_code}"
|
||||
exit 1
|
||||
fi
|
||||
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}/${date}.json.sig")
|
||||
if [[ ${http_code} -ne 404 ]]; then
|
||||
echo "Expected HTTP code 404 for: ${basepath}/${date}.json, but got ${http_code}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${configapi_cli} delete "${attestationVariant}" attestation-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.
|
||||
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}/${date_oldest}.json")
|
||||
if [[ ${http_code} -ne 404 ]]; then
|
||||
echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}"
|
||||
exit 1
|
||||
fi
|
||||
# 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.sig")
|
||||
if [[ ${http_code} -ne 404 ]]; then
|
||||
echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}"
|
||||
exit 1
|
||||
fi
|
78
api/attestationconfigapi/cli/main.go
Normal file
78
api/attestationconfigapi/cli/main.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/*
|
||||
This package provides a CLI to interact with the Attestationconfig API, a sub API of the Resource API.
|
||||
|
||||
You can execute an e2e test by running: `bazel run //internal/api/attestationconfigapi:configapi_e2e_test`.
|
||||
The CLI is used in the CI pipeline. Manual actions that change the bucket's data shouldn't be necessary.
|
||||
The reporter CLI caches the observed version values in a dedicated caching directory and derives the latest API version from it.
|
||||
Any version update is then pushed to the API.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
awsRegion = "eu-central-1"
|
||||
awsBucket = "cdn-constellation-backend"
|
||||
distributionID = constants.CDNDefaultDistributionID
|
||||
envCosignPwd = "COSIGN_PASSWORD"
|
||||
envCosignPrivateKey = "COSIGN_PRIVATE_KEY"
|
||||
// versionWindowSize defines the number of versions to be considered for the latest version.
|
||||
// Through our weekly e2e tests, each week 2 versions are uploaded:
|
||||
// One from a stable release, and one from a debug image.
|
||||
// A window size of 6 ensures we update only after a version has been "stable" for 3 weeks.
|
||||
versionWindowSize = 6
|
||||
)
|
||||
|
||||
var (
|
||||
// Cosign credentials.
|
||||
cosignPwd string
|
||||
privateKey string
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := newRootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// newRootCmd creates the root command.
|
||||
func newRootCmd() *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("bucket", "b", awsBucket, "bucket targeted by all operations.")
|
||||
rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.")
|
||||
|
||||
rootCmd.AddCommand(newUploadCmd())
|
||||
rootCmd.AddCommand(newDeleteCmd())
|
||||
rootCmd.AddCommand(newCompareCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
type apiConfig struct {
|
||||
url string
|
||||
distribution string
|
||||
cosignPublicKey string
|
||||
}
|
||||
|
||||
func getAPIEnvironment(testing bool) apiConfig {
|
||||
if testing {
|
||||
return apiConfig{url: "https://d33dzgxuwsgbpw.cloudfront.net", distribution: "ETZGUP1CWRC2P", cosignPublicKey: constants.CosignPublicKeyDev}
|
||||
}
|
||||
return apiConfig{url: constants.CDNRepositoryURL, distribution: constants.CDNDefaultDistributionID, cosignPublicKey: constants.CosignPublicKeyReleases}
|
||||
}
|
245
api/attestationconfigapi/cli/upload.go
Normal file
245
api/attestationconfigapi/cli/upload.go
Normal file
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi"
|
||||
"github.com/edgelesssys/constellation/v2/api/attestationconfigapi/cli/client"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/fetcher"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"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/google/go-tdx-guest/proto/tdx"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newUploadCmd() *cobra.Command {
|
||||
uploadCmd := &cobra.Command{
|
||||
Use: "upload VARIANT KIND FILE",
|
||||
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.\n"+
|
||||
"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.\n"+
|
||||
"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-sev-snp attestation-report /some/path/report.json",
|
||||
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(3), arg0isAttestationVariant(), 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.NewTextLogger(slog.LevelDebug).WithGroup("attestationconfigapi")
|
||||
|
||||
uploadCfg, err := newConfig(cmd, ([3]string)(args[:3]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing cli flags: %w", err)
|
||||
}
|
||||
|
||||
client, clientClose, err := client.New(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)
|
||||
}
|
||||
|
||||
return uploadReport(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log)
|
||||
}
|
||||
|
||||
func uploadReport(
|
||||
ctx context.Context, apiClient *client.Client,
|
||||
cfg uploadConfig, fs file.Handler, log *slog.Logger,
|
||||
) error {
|
||||
if cfg.kind != attestationReport {
|
||||
return fmt.Errorf("kind %s not supported", cfg.kind)
|
||||
}
|
||||
|
||||
apiFetcher := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey)
|
||||
latestVersionInAPI, err := apiFetcher.FetchLatestVersion(ctx, cfg.variant)
|
||||
if err != nil {
|
||||
var notFoundErr *fetcher.NotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
log.Info("No versions found in API, but assuming that we are uploading the first version.")
|
||||
} else {
|
||||
return fmt.Errorf("fetching latest version: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var newVersion, latestVersion any
|
||||
switch cfg.variant {
|
||||
case variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.GCPSEVSNP{}:
|
||||
latestVersion = latestVersionInAPI.SEVSNPVersion
|
||||
|
||||
log.Info(fmt.Sprintf("Reading SNP report from file: %s", cfg.path))
|
||||
newVersion, err = readSNPReport(cfg.path, fs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(fmt.Sprintf("Input SNP report: %+v", newVersion))
|
||||
|
||||
case variant.AzureTDX{}:
|
||||
latestVersion = latestVersionInAPI.TDXVersion
|
||||
|
||||
log.Info(fmt.Sprintf("Reading TDX report from file: %s", cfg.path))
|
||||
newVersion, err = readTDXReport(cfg.path, fs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(fmt.Sprintf("Input TDX report: %+v", newVersion))
|
||||
|
||||
default:
|
||||
return fmt.Errorf("variant %s not supported", cfg.variant)
|
||||
}
|
||||
|
||||
if err := apiClient.UploadLatestVersion(
|
||||
ctx, cfg.variant, newVersion, latestVersion, cfg.uploadDate, cfg.force,
|
||||
); err != nil && !errors.Is(err, client.ErrNoNewerVersion) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func convertQuoteToTDXVersion(quote *tdx.QuoteV4) attestationconfigapi.TDXVersion {
|
||||
return attestationconfigapi.TDXVersion{
|
||||
QESVN: binary.LittleEndian.Uint16(quote.Header.QeSvn),
|
||||
PCESVN: binary.LittleEndian.Uint16(quote.Header.PceSvn),
|
||||
QEVendorID: [16]byte(quote.Header.QeVendorId),
|
||||
XFAM: [8]byte(quote.TdQuoteBody.Xfam),
|
||||
TEETCBSVN: [16]byte(quote.TdQuoteBody.TeeTcbSvn),
|
||||
}
|
||||
}
|
||||
|
||||
type uploadConfig struct {
|
||||
variant variant.Variant
|
||||
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(client.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)
|
||||
}
|
||||
|
||||
variant, err := variant.FromString(args[0])
|
||||
if err != nil {
|
||||
return uploadConfig{}, fmt.Errorf("invalid attestation variant: %q: %w", args[0], err)
|
||||
}
|
||||
|
||||
kind := kindFromString(args[1])
|
||||
path := args[2]
|
||||
|
||||
return uploadConfig{
|
||||
variant: variant,
|
||||
kind: kind,
|
||||
path: path,
|
||||
uploadDate: uploadDate,
|
||||
cosignPublicKey: apiCfg.cosignPublicKey,
|
||||
region: region,
|
||||
bucket: bucket,
|
||||
url: apiCfg.url,
|
||||
distribution: apiCfg.distribution,
|
||||
force: force,
|
||||
cacheWindowSize: cacheWindowSize,
|
||||
}, nil
|
||||
}
|
60
api/attestationconfigapi/cli/validargs.go
Normal file
60
api/attestationconfigapi/cli/validargs.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func arg0isAttestationVariant() cobra.PositionalArgs {
|
||||
return func(_ *cobra.Command, args []string) error {
|
||||
attestationVariant, err := variant.FromString(args[0])
|
||||
if err != nil {
|
||||
return errors.New("argument 0 isn't a valid attestation variant")
|
||||
}
|
||||
switch attestationVariant {
|
||||
case variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.AzureTDX{}, variant.GCPSEVSNP{}:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("argument 0 isn't a supported attestation variant")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isValidKind(arg int) cobra.PositionalArgs {
|
||||
return func(_ *cobra.Command, args []string) error {
|
||||
if kind := kindFromString(args[arg]); kind == unknown {
|
||||
return fmt.Errorf("argument %s isn't a valid kind: must be one of [%q, %q]", args[arg], attestationReport, guestFirmware)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// objectKind encodes the available actions.
|
||||
type objectKind string
|
||||
|
||||
const (
|
||||
// unknown is the default objectKind and does nothing.
|
||||
unknown objectKind = "unknown-kind"
|
||||
attestationReport objectKind = "attestation-report"
|
||||
guestFirmware objectKind = "guest-firmware"
|
||||
)
|
||||
|
||||
func kindFromString(s string) objectKind {
|
||||
lower := strings.ToLower(s)
|
||||
switch objectKind(lower) {
|
||||
case attestationReport, guestFirmware:
|
||||
return objectKind(lower)
|
||||
default:
|
||||
return unknown
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue