Adrian Stobbe c446f36b0f
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>
2023-06-05 12:33:22 +02:00

229 lines
7.3 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package client
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
"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/logger"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/edgelesssys/constellation/v2/internal/variant"
)
// 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
fetcher fetcher.AttestationConfigAPIFetcher
}
// New returns a new Client.
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: s3Client,
s3ClientClose: clientClose,
signer: sigstore.NewSigner(cosignPwd, privateKey),
bucketID: cfg.Bucket,
fetcher: fetcher.New(),
}
return repo, clientClose, 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
}
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, version attestationconfig.AzureSEVSNPVersion, date time.Time) error {
variant := variant.AzureSEVSNP{}
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, 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 := a.createSignature(content, filePath)
if err != nil {
return err
}
if err := apiclient.Update(ctx, a.s3Client, signature); err != nil {
return fmt.Errorf("upload signature: %w", err)
}
return nil
}
// List returns the list of versions for the given attestation type.
func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) {
if attestation.Equal(variant.AzureSEVSNP{}) {
versions, err := apiclient.Fetch(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList{})
if 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 {
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
}
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, 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)))
return apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList(versions))
}
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
}
}
return nil, fmt.Errorf("version %s not found in list %v", versionStr, versions)
}
type deleteCmd struct {
apiObject apiclient.APIObject
}
func (d deleteCmd) Execute(ctx context.Context, c *apiclient.Client) error {
return apiclient.Delete(ctx, c, d.apiObject)
}
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
}