/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package attestationconfigapi import ( "context" "encoding/json" "fmt" "time" apiclient "github.com/edgelesssys/constellation/v2/internal/api/client" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/logger" "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 } // NewClient returns a new Client. func NewClient(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, } return repo, clientClose, nil } // UploadAzureSEVSNP uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. func (a Client) UploadAzureSEVSNP(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error { versions, err := a.List(ctx, variant.AzureSEVSNP{}) if err != nil { return fmt.Errorf("fetch version list: %w", err) } ops, err := a.uploadAzureSEVSNP(version, versions, date) if err != nil { return err } return executeAllCmds(ctx, a.s3Client, ops) } // 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 executeAllCmds(ctx, a.s3Client, ops) } // List returns the list of versions for the given attestation variant. func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) { if attestation.Equal(variant.AzureSEVSNP{}) { versions, err := apiclient.Fetch(ctx, a.s3Client, AzureSEVSNPVersionList{}) if err != nil { return nil, err } return versions, nil } return nil, fmt.Errorf("unsupported attestation variant: %s", attestation) } func (a Client) deleteAzureSEVSNPVersion(versions AzureSEVSNPVersionList, versionStr string) (ops []crudCmd, err error) { versionStr = versionStr + ".json" ops = append(ops, deleteCmd{ apiObject: AzureSEVSNPVersionAPI{ Version: versionStr, }, }) ops = append(ops, deleteCmd{ apiObject: AzureSEVSNPVersionSignature{ Version: versionStr, }, }) removedVersions, err := removeVersion(versions, versionStr) if err != nil { return nil, err } ops = append(ops, putCmd{ apiObject: removedVersions, }) return ops, nil } func (a Client) uploadAzureSEVSNP(versions AzureSEVSNPVersion, versionNames []string, date time.Time) (res []crudCmd, err error) { dateStr := date.Format(VersionFormat) + ".json" res = append(res, putCmd{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{AzureSEVSNPVersionList(newVersions)}) return } func (a Client) createSignature(content []byte, dateStr string) (res AzureSEVSNPVersionSignature, err error) { signature, err := a.signer.Sign(content) if err != nil { return res, fmt.Errorf("sign version file: %w", err) } return AzureSEVSNPVersionSignature{ Signature: signature, Version: dateStr, }, nil } func removeVersion(versions AzureSEVSNPVersionList, versionStr string) (removedVersions 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 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.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) } 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 } func addVersion(versions []string, newVersion string) []string { versions = append(versions, newVersion) versions = variant.RemoveDuplicate(versions) SortAzureSEVSNPVersionList(versions) return versions }