config: Azure SNP tool can delete specific version from attestation API (#1863)

* client supports delete version

* rename to new attestation / fetcher naming

* add delete command to upload tool

* test client delete

* bazel update

* use general client in attestation client

* Update hack/configapi/cmd/delete.go

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

* daniel feedback

* unit test azure sev upload

* Update hack/configapi/cmd/delete.go

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

* add client integration test

* new client cmds use apiObject

---------

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
This commit is contained in:
Adrian Stobbe 2023-06-05 12:33:22 +02:00 committed by GitHub
parent 315b6c2f01
commit c446f36b0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 549 additions and 228 deletions

View File

@ -16,7 +16,7 @@ import (
"net/url"
"testing"
configapi "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
@ -281,7 +281,7 @@ func TestConfigFetchMeasurements(t *testing.T) {
require.NoError(err)
cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t)}
err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, fakeConfigFetcher{}, client)
err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, fakeAttestationFetcher{}, client)
if tc.wantErr {
assert.Error(err)
return
@ -291,27 +291,27 @@ func TestConfigFetchMeasurements(t *testing.T) {
}
}
type fakeConfigFetcher struct{}
type fakeAttestationFetcher struct{}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ configapi.AzureSEVSNPVersionList) (configapi.AzureSEVSNPVersionList, error) {
return configapi.AzureSEVSNPVersionList(
func (f fakeAttestationFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ attestationconfig.AzureSEVSNPVersionList) (attestationconfig.AzureSEVSNPVersionList, error) {
return attestationconfig.AzureSEVSNPVersionList(
[]string{},
), nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
func (f fakeAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ attestationconfig.AzureSEVSNPVersionAPI) (attestationconfig.AzureSEVSNPVersionAPI, error) {
return attestationconfig.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg,
}, nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
func (f fakeAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfig.AzureSEVSNPVersionAPI, error) {
return attestationconfig.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg,
}, nil
}
var testCfg = configapi.AzureSEVSNPVersion{
var testCfg = attestationconfig.AzureSEVSNPVersion{
Microcode: 93,
TEE: 0,
SNP: 6,

View File

@ -202,7 +202,7 @@ func TestCreate(t *testing.T) {
fileHandler := file.NewHandler(tc.setupFs(require, tc.provider))
c := &createCmd{log: logger.NewTest(t)}
err := c.create(cmd, tc.creator, fileHandler, &nopSpinner{}, fakeConfigFetcher{})
err := c.create(cmd, tc.creator, fileHandler, &nopSpinner{}, fakeAttestationFetcher{})
if tc.wantErr {
assert.Error(err)

View File

@ -175,7 +175,7 @@ func TestInitialize(t *testing.T) {
defer cancel()
cmd.SetContext(ctx)
i := &initCmd{log: logger.NewTest(t), spinner: &nopSpinner{}}
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeConfigFetcher{})
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeAttestationFetcher{})
if tc.wantErr {
assert.Error(err)
@ -519,7 +519,7 @@ func TestAttestation(t *testing.T) {
cmd.SetContext(ctx)
i := &initCmd{log: logger.NewTest(t), spinner: &nopSpinner{}}
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeConfigFetcher{})
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeAttestationFetcher{})
assert.Error(err)
// make sure the error is actually a TLS handshake error
assert.Contains(err.Error(), "transport: authentication handshake failed")

View File

@ -163,7 +163,7 @@ func TestRecover(t *testing.T) {
))
newDialer := func(atls.Validator) *dialer.Dialer { return nil }
r := &recoverCmd{log: logger.NewTest(t), configFetcher: fakeConfigFetcher{}}
r := &recoverCmd{log: logger.NewTest(t), configFetcher: fakeAttestationFetcher{}}
err := r.recover(cmd, fileHandler, time.Millisecond, tc.doer, newDialer)
if tc.wantErr {
assert.Error(err)

View File

@ -142,7 +142,7 @@ func TestUpgradeApply(t *testing.T) {
require.NoError(handler.WriteYAML(constants.ConfigFilename, cfg))
require.NoError(handler.WriteJSON(constants.ClusterIDsFileName, clusterid.File{}))
upgrader := upgradeApplyCmd{upgrader: tc.upgrader, log: logger.NewTest(t), imageFetcher: tc.fetcher, configFetcher: fakeConfigFetcher{}}
upgrader := upgradeApplyCmd{upgrader: tc.upgrader, log: logger.NewTest(t), imageFetcher: tc.fetcher, configFetcher: fakeAttestationFetcher{}}
err := upgrader.upgradeApply(cmd, handler)
if tc.wantErr {
assert.Error(err)

View File

@ -271,7 +271,7 @@ func TestUpgradeCheck(t *testing.T) {
cmd := newUpgradeCheckCmd()
err := checkCmd.upgradeCheck(cmd, fileHandler, fakeConfigFetcher{}, tc.flags)
err := checkCmd.upgradeCheck(cmd, fileHandler, fakeAttestationFetcher{}, tc.flags)
if tc.wantError {
assert.Error(err)
return

View File

@ -190,7 +190,7 @@ func TestVerify(t *testing.T) {
}
v := &verifyCmd{log: logger.NewTest(t)}
err := v.verify(cmd, fileHandler, tc.protoClient, tc.formatter, fakeConfigFetcher{})
err := v.verify(cmd, fileHandler, tc.protoClient, tc.formatter, fakeAttestationFetcher{})
if tc.wantErr {
assert.Error(err)

View File

@ -3,24 +3,33 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "cmd",
srcs = ["root.go"],
srcs = [
"delete.go",
"root.go",
],
importpath = "github.com/edgelesssys/constellation/v2/hack/configapi/cmd",
visibility = ["//visibility:public"],
deps = [
"//internal/api/attestationconfig",
"//internal/api/attestationconfig/client",
"//internal/api/attestationconfig/fetcher",
"//internal/logger",
"//internal/staticupload",
"@com_github_spf13_cobra//:cobra",
"@org_uber_go_zap//:zap",
],
)
go_test(
name = "cmd_test",
srcs = ["root_test.go"],
srcs = [
"delete_test.go",
"root_test.go",
],
embed = [":cmd"],
deps = [
"//internal/api/attestationconfig",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)

View File

@ -0,0 +1,63 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"context"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"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",
Short: "delete a specific version from the config api",
RunE: runDelete,
}
cmd.Flags().StringP("version", "v", "", "Name of the version to delete (without .json suffix)")
must(enforceRequiredFlags(cmd, "version"))
return cmd
}
type deleteCmd struct {
attestationClient deleteClient
}
type deleteClient interface {
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) error {
cfg := staticupload.Config{
Bucket: awsBucket,
Region: awsRegion,
}
repo, closefn, err := client.New(cmd.Context(), cfg, []byte(cosignPwd), []byte(privateKey), false, log())
if err != nil {
return fmt.Errorf("create attestation client: %w", err)
}
defer func() {
if err := closefn(cmd.Context()); err != nil {
cmd.Printf("close client: %s\n", err.Error())
}
}()
deleteCmd := deleteCmd{
attestationClient: repo,
}
return deleteCmd.delete(cmd)
}

View File

@ -0,0 +1,38 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
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

@ -16,6 +16,8 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
attestationconfigclient "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher"
"github.com/edgelesssys/constellation/v2/internal/logger"
"go.uber.org/zap"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/spf13/cobra"
@ -54,8 +56,8 @@ func newRootCmd() *cobra.Command {
RunE: runCmd,
}
rootCmd.PersistentFlags().StringVarP(&versionFilePath, "version-file", "f", "", "File path to the version json file.")
must(enforceRequiredFlags(rootCmd, "version-file"))
must(enforcePersistentRequiredFlags(rootCmd, "version-file"))
rootCmd.AddCommand(newDeleteCmd())
return rootCmd
}
@ -93,11 +95,11 @@ func runCmd(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("comparing versions: %w", err)
}
if isNewer {
fmt.Printf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion)
sut, sutClose, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), []byte(privateKey))
cmd.Printf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion)
sut, sutClose, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), []byte(privateKey), false, log())
defer func() {
if err := sutClose(ctx); err != nil {
fmt.Printf("closing repo: %v\n", err)
cmd.Printf("closing repo: %v\n", err)
}
}()
if err != nil {
@ -143,6 +145,15 @@ func isInputNewerThanLatestAPI(input, latest attestationconfig.AzureSEVSNPVersio
}
func enforceRequiredFlags(cmd *cobra.Command, flags ...string) error {
for _, flag := range flags {
if err := cmd.MarkFlagRequired(flag); err != nil {
return err
}
}
return nil
}
func enforcePersistentRequiredFlags(cmd *cobra.Command, flags ...string) error {
for _, flag := range flags {
if err := cmd.MarkPersistentFlagRequired(flag); err != nil {
return err
@ -156,3 +167,7 @@ func must(err error) {
panic(err)
}
}
func log() *logger.Logger {
return logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfig")
}

View File

@ -34,29 +34,53 @@ type AzureSEVSNPVersion struct {
Microcode uint8 `json:"microcode"`
}
// AzureSEVSNPVersionGet is the request to get the version information of the specific version in the config api.
type AzureSEVSNPVersionGet struct {
// AzureSEVSNPVersionSignature is the object to perform CRUD operations on the config api.
type AzureSEVSNPVersionSignature struct {
Version string `json:"-"`
Signature []byte `json:"signature"`
}
// JSONPath returns the path to the JSON file for the request to the config api.
func (s AzureSEVSNPVersionSignature) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), s.Version, ".sig")
}
// URL returns the URL for the request to the config api.
func (s AzureSEVSNPVersionSignature) URL() (string, error) {
return getURL(s)
}
// ValidateRequest validates the request.
func (s AzureSEVSNPVersionSignature) ValidateRequest() error {
if !strings.HasSuffix(s.Version, ".json") {
return fmt.Errorf("version has no .json suffix")
}
return nil
}
// Validate is a No-Op at the moment.
func (s AzureSEVSNPVersionSignature) Validate() error {
return nil
}
// AzureSEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
type AzureSEVSNPVersionAPI struct {
Version string `json:"-"`
AzureSEVSNPVersion
}
// URL returns the URL for the request to the config api.
func (i AzureSEVSNPVersionGet) URL() (string, error) {
url, err := url.Parse(constants.CDNRepositoryURL)
if err != nil {
return "", fmt.Errorf("parsing CDN URL: %w", err)
}
url.Path = i.JSONPath()
return url.String(), nil
func (i AzureSEVSNPVersionAPI) URL() (string, error) {
return getURL(i)
}
// JSONPath returns the path to the JSON file for the request to the config api.
func (i AzureSEVSNPVersionGet) JSONPath() string {
func (i AzureSEVSNPVersionAPI) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), i.Version)
}
// ValidateRequest validates the request.
func (i AzureSEVSNPVersionGet) ValidateRequest() error {
func (i AzureSEVSNPVersionAPI) ValidateRequest() error {
if !strings.HasSuffix(i.Version, ".json") {
return fmt.Errorf("version has no .json suffix")
}
@ -64,7 +88,7 @@ func (i AzureSEVSNPVersionGet) ValidateRequest() error {
}
// Validate is a No-Op at the moment.
func (i AzureSEVSNPVersionGet) Validate() error {
func (i AzureSEVSNPVersionAPI) Validate() error {
return nil
}
@ -73,12 +97,7 @@ type AzureSEVSNPVersionList []string
// URL returns the URL for the request to the config api.
func (i AzureSEVSNPVersionList) URL() (string, error) {
url, err := url.Parse(constants.CDNRepositoryURL)
if err != nil {
return "", fmt.Errorf("parsing CDN URL: %w", err)
}
url.Path = i.JSONPath()
return url.String(), nil
return getURL(i)
}
// JSONPath returns the path to the JSON file for the request to the config api.
@ -98,3 +117,16 @@ func (i AzureSEVSNPVersionList) Validate() error {
}
return nil
}
func getURL(obj jsoPather) (string, error) {
url, err := url.Parse(constants.CDNRepositoryURL)
if err != nil {
return "", fmt.Errorf("parsing CDN URL: %w", err)
}
url.Path = obj.JSONPath()
return url.String(), nil
}
type jsoPather interface {
JSONPath() string
}

View File

@ -8,13 +8,13 @@ go_library(
visibility = ["//:__subpackages__"],
deps = [
"//internal/api/attestationconfig",
"//internal/api/attestationconfig/fetcher",
"//internal/api/client",
"//internal/constants",
"//internal/kms/storage",
"//internal/logger",
"//internal/sigstore",
"//internal/staticupload",
"//internal/variant",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
],
)
@ -23,14 +23,13 @@ go_test(
srcs = ["client_test.go"],
# keep
count = 1,
embed = [":client"],
# keep
gotags = ["e2e"],
# keep
tags = ["manual"],
deps = [
":client",
"//internal/api/attestationconfig",
"//internal/staticupload",
"@com_github_stretchr_testify//require",
"@com_github_stretchr_testify//assert",
],
)

View File

@ -6,21 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only
package client
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"path"
"sort"
"time"
s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher"
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/kms/storage"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/edgelesssys/constellation/v2/internal/variant"
@ -28,70 +24,90 @@ import (
// Client manages (modifies) the version information for the attestation variants.
type Client struct {
s3Client
s3Client *apiclient.Client
s3ClientClose func(ctx context.Context) error
bucketID string
cosignPwd []byte // used to decrypt the cosign private key
privKey []byte // used to sign
signer sigstore.Signer
fetcher fetcher.AttestationConfigAPIFetcher
}
// New returns a new Client.
func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte) (*Client, CloseFunc, error) {
client, clientClose, err := staticupload.New(ctx, cfg)
func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte, dryRun bool, log *logger.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: client,
s3Client: s3Client,
s3ClientClose: clientClose,
signer: sigstore.NewSigner(cosignPwd, privateKey),
bucketID: cfg.Bucket,
cosignPwd: cosignPwd,
privKey: privateKey,
fetcher: fetcher.New(),
}
repoClose := func(ctx context.Context) error {
return repo.Close(ctx)
}
return repo, repoClose, nil
return repo, clientClose, nil
}
// Close closes the Client.
func (a Client) Close(ctx context.Context) error {
if a.s3ClientClose == nil {
return nil
func (a Client) uploadAzureSEVSNP(versions attestationconfig.AzureSEVSNPVersion, versionNames []string, date time.Time) (res []putCmd, err error) {
dateStr := date.Format("2006-01-02-15-04") + ".json"
res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: versions}})
versionBytes, err := json.Marshal(versions)
if err != nil {
return res, err
}
return a.s3ClientClose(ctx)
signature, err := a.createSignature(versionBytes, dateStr)
if err != nil {
return res, err
}
res = append(res, putCmd{signature})
newVersions := addVersion(versionNames, dateStr)
res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionList(newVersions)})
return
}
// UploadAzureSEVSNP uploads the latest version numbers of the Azure SEVSNP.
func (a Client) UploadAzureSEVSNP(ctx context.Context, versions attestationconfig.AzureSEVSNPVersion, date time.Time) error {
versionBytes, err := json.Marshal(versions)
if err != nil {
return err
}
func (a Client) UploadAzureSEVSNP(ctx context.Context, version attestationconfig.AzureSEVSNPVersion, date time.Time) error {
variant := variant.AzureSEVSNP{}
fname := date.Format("2006-01-02-15-04") + ".json"
filePath := fmt.Sprintf("%s/%s/%s", constants.CDNAttestationConfigPrefixV1, variant.String(), fname)
err = put(ctx, a.s3Client, a.bucketID, filePath, versionBytes)
dateStr := date.Format("2006-01-02-15-04") + ".json"
err := apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: version})
if err != nil {
return err
}
versionBytes, err := json.Marshal(version)
if err != nil {
return err
}
filePath := fmt.Sprintf("%s/%s/%s", constants.CDNAttestationConfigPrefixV1, variant.String(), dateStr)
err = a.createAndUploadSignature(ctx, versionBytes, filePath)
if err != nil {
return err
}
return a.addVersionToList(ctx, variant, fname)
return a.addVersionToList(ctx, variant, dateStr)
}
func (a Client) createSignature(content []byte, dateStr string) (res attestationconfig.AzureSEVSNPVersionSignature, err error) {
signature, err := a.signer.Sign(content)
if err != nil {
return res, fmt.Errorf("sign version file: %w", err)
}
return attestationconfig.AzureSEVSNPVersionSignature{
Signature: signature,
Version: dateStr,
}, nil
}
// createAndUploadSignature signs the given content and uploads the signature to the given filePath with the .sig suffix.
func (a Client) createAndUploadSignature(ctx context.Context, content []byte, filePath string) error {
signature, err := sigstore.SignContent(a.cosignPwd, a.privKey, content)
signature, err := a.createSignature(content, filePath)
if err != nil {
return fmt.Errorf("sign version file: %w", err)
return err
}
err = put(ctx, a.s3Client, a.bucketID, filePath+".sig", signature)
if err != nil {
if err := apiclient.Update(ctx, a.s3Client, signature); err != nil {
return fmt.Errorf("upload signature: %w", err)
}
return nil
@ -99,81 +115,114 @@ func (a Client) createAndUploadSignature(ctx context.Context, content []byte, fi
// List returns the list of versions for the given attestation type.
func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) {
key := path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list")
bt, err := get(ctx, a.s3Client, a.bucketID, key)
if err != nil {
return nil, err
if attestation.Equal(variant.AzureSEVSNP{}) {
versions, err := apiclient.Fetch(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList{})
if err != nil {
return nil, err
}
return versions, nil
}
var versions []string
if err := json.Unmarshal(bt, &versions); err != nil {
return nil, err
}
return versions, nil
return nil, fmt.Errorf("unsupported attestation type: %s", attestation)
}
// DeleteList empties the list of versions for the given attestation type.
func (a Client) DeleteList(ctx context.Context, attestation variant.Variant) error {
versions := []string{}
bt, err := json.Marshal(&versions)
if attestation.Equal(variant.AzureSEVSNP{}) {
return apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList{})
}
return fmt.Errorf("unsupported attestation type: %s", attestation)
}
func (a Client) deleteAzureSEVSNPVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr string) (ops []crudOPNew, err error) {
versionStr = versionStr + ".json"
ops = append(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionAPI{
Version: versionStr,
},
})
ops = append(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionSignature{
Version: versionStr,
},
})
removedVersions, err := removeVersion(versions, versionStr)
if err != nil {
return nil, err
}
ops = append(ops, putCmd{
apiObject: removedVersions,
})
return ops, nil
}
// DeleteAzureSEVSNPVersion deletes the given version (without .json suffix) from the API.
func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error {
versions, err := a.List(ctx, variant.AzureSEVSNP{})
if err != nil {
return fmt.Errorf("fetch version list: %w", err)
}
ops, err := a.deleteAzureSEVSNPVersion(versions, versionStr)
if err != nil {
return err
}
return put(ctx, a.s3Client, a.bucketID, path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list"), bt)
for _, op := range ops {
if err := op.Execute(ctx, a.s3Client); err != nil {
return fmt.Errorf("execute operation %+v: %w", op, err)
}
}
return nil
}
func (a Client) addVersionToList(ctx context.Context, attestation variant.Variant, fname string) error {
versions := []string{}
key := path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list")
bt, err := get(ctx, a.s3Client, a.bucketID, key)
if err == nil {
if err := json.Unmarshal(bt, &versions); err != nil {
return err
}
} else if !errors.Is(err, storage.ErrDEKUnset) {
versions, err := a.List(ctx, attestation)
if err != nil {
return err
}
versions = append(versions, fname)
versions = variant.RemoveDuplicate(versions)
sort.Sort(sort.Reverse(sort.StringSlice(versions)))
json, err := json.Marshal(versions)
if err != nil {
return err
}
return put(ctx, a.s3Client, a.bucketID, key, json)
return apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList(versions))
}
// get is a convenience method.
func get(ctx context.Context, client s3Client, bucket, path string) ([]byte, error) {
getObjectInput := &s3.GetObjectInput{
Bucket: &bucket,
Key: &path,
func removeVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr string) (removedVersions attestationconfig.AzureSEVSNPVersionList, err error) {
for i, v := range versions {
if v == versionStr {
if i == len(versions)-1 {
removedVersions = versions[:i]
} else {
removedVersions = append(versions[:i], versions[i+1:]...)
}
return removedVersions, nil
}
}
output, err := client.GetObject(ctx, getObjectInput)
if err != nil {
return nil, fmt.Errorf("getting object: %w", err)
}
return io.ReadAll(output.Body)
return nil, fmt.Errorf("version %s not found in list %v", versionStr, versions)
}
// put is a convenience method.
func put(ctx context.Context, client s3Client, bucket, path string, data []byte) error {
putObjectInput := &s3.PutObjectInput{
Bucket: &bucket,
Key: &path,
Body: bytes.NewReader(data),
}
_, err := client.Upload(ctx, putObjectInput)
return err
type deleteCmd struct {
apiObject apiclient.APIObject
}
type s3Client interface {
GetObject(
ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options),
) (*s3.GetObjectOutput, error)
Upload(
ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader),
) (*s3manager.UploadOutput, error)
func (d deleteCmd) Execute(ctx context.Context, c *apiclient.Client) error {
return apiclient.Delete(ctx, c, d.apiObject)
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error
type putCmd struct {
apiObject apiclient.APIObject
}
func (p putCmd) Execute(ctx context.Context, c *apiclient.Client) error {
return apiclient.Update(ctx, c, p.apiObject)
}
type crudOPNew interface {
Execute(ctx context.Context, c *apiclient.Client) error
}
func addVersion(versions []string, newVersion string) []string {
versions = append(versions, newVersion)
versions = variant.RemoveDuplicate(versions)
sort.Sort(sort.Reverse(sort.StringSlice(versions)))
return versions
}

View File

@ -1,82 +1,74 @@
//go:build e2e
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package client_test
package client
import (
"context"
"flag"
"fmt"
"io"
"os"
"testing"
"time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)
const (
awsBucket = "cdn-constellation-backend"
awsRegion = "eu-central-1"
envAwsKeyID = "AWS_ACCESS_KEY_ID"
envAwsKey = "AWS_ACCESS_KEY"
)
var cfg staticupload.Config
var (
cosignPwd = flag.String("cosign-pwd", "", "Password to decrypt the cosign private key. Required for signing.")
privateKeyPath = flag.String("private-key", "", "Path to the private key used for signing. Required for signing.")
privateKey []byte
)
func TestMain(m *testing.M) {
flag.Parse()
if *cosignPwd == "" || *privateKeyPath == "" {
flag.Usage()
fmt.Println("Required flags not set: --cosign-pwd, --private-key. Skipping tests.")
os.Exit(1)
func TestUploadAzureSEVSNP(t *testing.T) {
sut := Client{
bucketID: "bucket",
signer: fakeSigner{},
}
if _, present := os.LookupEnv(envAwsKey); !present {
fmt.Printf("%s not set. Skipping tests.\n", envAwsKey)
os.Exit(1)
}
if _, present := os.LookupEnv(envAwsKeyID); !present {
fmt.Printf("%s not set. Skipping tests.\n", envAwsKeyID)
os.Exit(1)
}
cfg = staticupload.Config{
Bucket: awsBucket,
Region: awsRegion,
}
file, _ := os.Open(*privateKeyPath)
var err error
privateKey, err = io.ReadAll(file)
if err != nil {
panic(err)
}
os.Exit(m.Run())
version := attestationconfig.AzureSEVSNPVersion{}
date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
ops, err := sut.uploadAzureSEVSNP(version, []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, date)
assert := assert.New(t)
assert.NoError(err)
dateStr := "2023-01-01-01-01.json"
assert.Contains(ops, putCmd{
apiObject: attestationconfig.AzureSEVSNPVersionAPI{
Version: dateStr,
AzureSEVSNPVersion: version,
},
})
assert.Contains(ops, putCmd{
apiObject: attestationconfig.AzureSEVSNPVersionSignature{
Version: dateStr,
Signature: []byte("signature"),
},
})
assert.Contains(ops, putCmd{
apiObject: attestationconfig.AzureSEVSNPVersionList([]string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}),
})
}
var versionValues = attestationconfig.AzureSEVSNPVersion{
Bootloader: 2,
TEE: 0,
SNP: 6,
Microcode: 93,
func TestDeleteAzureSEVSNPVersions(t *testing.T) {
sut := Client{
bucketID: "bucket",
}
versions := attestationconfig.AzureSEVSNPVersionList([]string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"})
ops, err := sut.deleteAzureSEVSNPVersion(versions, "2021-01-01")
assert := assert.New(t)
assert.NoError(err)
assert.Contains(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionAPI{
Version: "2021-01-01.json",
},
})
assert.Contains(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionSignature{
Version: "2021-01-01.json",
},
})
assert.Contains(ops, putCmd{
apiObject: attestationconfig.AzureSEVSNPVersionList([]string{"2023-01-01.json", "2019-01-01.json"}),
})
}
func TestUploadAzureSEVSNPVersions(t *testing.T) {
ctx := context.Background()
client, clientClose, err := client.New(ctx, cfg, []byte(*cosignPwd), privateKey)
require.NoError(t, err)
defer func() { _ = clientClose(ctx) }()
d := time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)
require.NoError(t, client.UploadAzureSEVSNP(ctx, versionValues, d))
type fakeSigner struct{}
func (fakeSigner) Sign(_ []byte) ([]byte, error) {
return []byte("signature"), nil
}

View File

@ -0,0 +1,84 @@
//go:build integration
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package test
import (
"context"
"flag"
"fmt"
"io"
"os"
"testing"
"time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
const (
awsBucket = "cdn-constellation-backend"
awsRegion = "eu-central-1"
envAwsKeyID = "AWS_ACCESS_KEY_ID"
envAwsKey = "AWS_ACCESS_KEY"
)
var cfg staticupload.Config
var (
cosignPwd = flag.String("cosign-pwd", "", "Password to decrypt the cosign private key. Required for signing.")
privateKeyPath = flag.String("private-key", "", "Path to the private key used for signing. Required for signing.")
privateKey []byte
)
func TestMain(m *testing.M) {
flag.Parse()
if *cosignPwd == "" || *privateKeyPath == "" {
flag.Usage()
fmt.Println("Required flags not set: --cosign-pwd, --private-key. Skipping tests.")
os.Exit(1)
}
if _, present := os.LookupEnv(envAwsKey); !present {
fmt.Printf("%s not set. Skipping tests.\n", envAwsKey)
os.Exit(1)
}
if _, present := os.LookupEnv(envAwsKeyID); !present {
fmt.Printf("%s not set. Skipping tests.\n", envAwsKeyID)
os.Exit(1)
}
cfg = staticupload.Config{
Bucket: awsBucket,
Region: awsRegion,
}
file, _ := os.Open(*privateKeyPath)
var err error
privateKey, err = io.ReadAll(file)
if err != nil {
panic(err)
}
os.Exit(m.Run())
}
var versionValues = attestationconfig.AzureSEVSNPVersion{
Bootloader: 2,
TEE: 0,
SNP: 6,
Microcode: 93,
}
func TestUploadAzureSEVSNPVersions(t *testing.T) {
ctx := context.Background()
client, clientClose, err := client.New(ctx, cfg, []byte(*cosignPwd), privateKey, false, logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfig"))
require.NoError(t, err)
defer func() { _ = clientClose(ctx) }()
d := time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)
require.NoError(t, client.UploadAzureSEVSNP(ctx, versionValues, d))
}

View File

@ -24,9 +24,9 @@ const cosignPublicKey = constants.CosignPublicKeyReleases
// AttestationConfigAPIFetcher fetches config API resources without authentication.
type AttestationConfigAPIFetcher interface {
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionGet) (attestationconfig.AzureSEVSNPVersionGet, error)
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionAPI) (attestationconfig.AzureSEVSNPVersionAPI, error)
FetchAzureSEVSNPVersionList(ctx context.Context, attestation attestationconfig.AzureSEVSNPVersionList) (attestationconfig.AzureSEVSNPVersionList, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context) (attestationconfig.AzureSEVSNPVersionGet, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context) (attestationconfig.AzureSEVSNPVersionAPI, error)
}
// Fetcher fetches AttestationCfg API resources without authentication.
@ -50,7 +50,7 @@ func (f *Fetcher) FetchAzureSEVSNPVersionList(ctx context.Context, attestation a
}
// FetchAzureSEVSNPVersion fetches the version information from the config API.
func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionGet) (attestationconfig.AzureSEVSNPVersionGet, error) {
func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionAPI) (attestationconfig.AzureSEVSNPVersionAPI, error) {
urlString, err := azureVersion.URL()
if err != nil {
return azureVersion, err
@ -77,13 +77,13 @@ func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion atte
}
// FetchAzureSEVSNPVersionLatest returns the latest versions of the given type.
func (f *Fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res attestationconfig.AzureSEVSNPVersionGet, err error) {
func (f *Fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res attestationconfig.AzureSEVSNPVersionAPI, err error) {
var list attestationconfig.AzureSEVSNPVersionList
list, err = f.FetchAzureSEVSNPVersionList(ctx, list)
if err != nil {
return res, fmt.Errorf("fetching versions list: %w", err)
}
get := attestationconfig.AzureSEVSNPVersionGet{Version: list[0]} // get latest version (as sorted reversely alphanumerically)
get := attestationconfig.AzureSEVSNPVersionAPI{Version: list[0]} // get latest version (as sorted reversely alphanumerically)
get, err = f.FetchAzureSEVSNPVersion(ctx, get)
if err != nil {
return res, fmt.Errorf("failed fetching version: %w", err)

View File

@ -18,7 +18,7 @@ import (
"github.com/stretchr/testify/assert"
)
var testCfg = configapi.AzureSEVSNPVersionGet{
var testCfg = configapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: configapi.AzureSEVSNPVersion{
Microcode: 93,
TEE: 0,
@ -31,7 +31,7 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
testcases := map[string]struct {
signature []byte
wantErr bool
want configapi.AzureSEVSNPVersionGet
want configapi.AzureSEVSNPVersionAPI
}{
"get version with valid signature": {
signature: []byte("MEQCIBPEbYg89MIQuaGStLhKGLGMKvKFoYCaAniDLwoIwulqAiB+rj7KMaMOMGxmUsjI7KheCXSNM8NzN+tuDw6AywI75A=="), // signed with release key

View File

@ -43,10 +43,9 @@ import (
"go.uber.org/zap"
)
// Client is the client for the versions API.
// Client is the a general client for all APIs.
type Client struct {
uploadClient uploadClient
s3Client s3Client
s3Client
s3ClientClose func(ctx context.Context) error
bucket string
cacheInvalidationWaitTimeout time.Duration
@ -101,7 +100,6 @@ func NewClient(ctx context.Context, region, bucket, distributionID string, dryRu
}
client := &Client{
uploadClient: staticUploadClient,
s3Client: staticUploadClient,
s3ClientClose: staticUploadClientClose,
bucket: bucket,
@ -179,14 +177,15 @@ func ptr[T any](t T) *T {
return &t
}
type apiObject interface {
// APIObject is an object that is used to perform CRUD operations on the API.
type APIObject interface {
ValidateRequest() error
Validate() error
JSONPath() string
}
// Fetch fetches the given apiObject from the public Constellation CDN.
func Fetch[T apiObject](ctx context.Context, c *Client, obj T) (T, error) {
func Fetch[T APIObject](ctx context.Context, c *Client, obj T) (T, error) {
if err := obj.ValidateRequest(); err != nil {
return *new(T), fmt.Errorf("validating request for %T: %w", obj, err)
}
@ -218,8 +217,8 @@ func Fetch[T apiObject](ctx context.Context, c *Client, obj T) (T, error) {
return newObj, nil
}
// Update creates/updates the given apiObject in the public Constellation CDN.
func Update[T apiObject](ctx context.Context, c *Client, obj T) error {
// Update creates/updates the given apiObject in the public Constellation API.
func Update(ctx context.Context, c *Client, obj APIObject) error {
if err := obj.Validate(); err != nil {
return fmt.Errorf("validating %T struct: %w", obj, err)
}
@ -243,13 +242,32 @@ func Update[T apiObject](ctx context.Context, c *Client, obj T) error {
c.dirtyPaths = append(c.dirtyPaths, "/"+obj.JSONPath())
c.Log.Debugf("Uploading %T to s3: %v", obj, obj.JSONPath())
if _, err := c.uploadClient.Upload(ctx, in); err != nil {
if _, err := c.Upload(ctx, in); err != nil {
return fmt.Errorf("uploading %T: %w", obj, err)
}
return nil
}
// Delete deletes the given apiObject from the public Constellation API.
func Delete(ctx context.Context, c *Client, obj APIObject) error {
if err := obj.ValidateRequest(); err != nil {
return fmt.Errorf("validating request for %T: %w", obj, err)
}
in := &s3.DeleteObjectInput{
Bucket: &c.bucket,
Key: ptr(obj.JSONPath()),
}
c.Log.Debugf("Deleting %T from s3: %s", obj, obj.JSONPath())
if _, err := c.DeleteObject(ctx, in); err != nil {
return fmt.Errorf("deleting s3 object at %s: %w", obj.JSONPath(), err)
}
return nil
}
// NotFoundError is an error that is returned when a resource is not found.
type NotFoundError struct {
err error
@ -273,6 +291,10 @@ type s3Client interface {
DeleteObjects(
ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options),
) (*s3.DeleteObjectsOutput, error)
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput,
optFns ...func(*s3.Options),
) (*s3.DeleteObjectOutput, error)
uploadClient
}
type uploadClient interface {

View File

@ -900,14 +900,14 @@ func (f fakeConfigFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ conf
), nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionAPI) (configapi.AzureSEVSNPVersionAPI, error) {
return configapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg,
}, nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionAPI, error) {
return configapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg,
}, nil
}

View File

@ -26,6 +26,29 @@ const (
sigstorePrivateKeyPemType = "ENCRYPTED SIGSTORE PRIVATE KEY"
)
// Signer is used to sign the version file. Used for unit testing.
type Signer interface {
Sign(content []byte) (res []byte, err error)
}
// NewSigner returns a new Signer.
func NewSigner(cosignPwd, privKey []byte) Signer {
return signer{cosignPwd: cosignPwd, privKey: privKey}
}
type signer struct {
cosignPwd []byte // used to decrypt the cosign private key
privKey []byte // used to sign
}
func (s signer) Sign(content []byte) (signature []byte, err error) {
signature, err = SignContent(s.cosignPwd, s.privKey, content)
if err != nil {
return signature, fmt.Errorf("sign version file: %w", err)
}
return
}
// SignContent signs the content with the cosign encrypted private key and corresponding cosign password.
func SignContent(password, encryptedPrivateKey, content []byte) ([]byte, error) {
sv, err := loadPrivateKey(encryptedPrivateKey, password)

View File

@ -93,7 +93,7 @@ func (e InvalidationError) Unwrap() error {
return e.inner
}
// New creates a new Client.
// New creates a new Client. Call CloseFunc when done with operations.
func New(ctx context.Context, config Config) (*Client, CloseFunc, error) {
config.SetsDefault()
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(config.Region))
@ -114,12 +114,7 @@ func New(ctx context.Context, config Config) (*Client, CloseFunc, error) {
cacheInvalidationWaitTimeout: config.CacheInvalidationWaitTimeout,
bucketID: config.Bucket,
}
clientClose := func(ctx context.Context) error {
// ensure that all keys are invalidated
return client.Flush(ctx)
}
return client, clientClose, nil
return client, client.Flush, nil
}
// Flush flushes the client by invalidating the CDN cache for modified keys.