Enable upload of TDX reports to Constellation CDN

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2024-06-12 16:30:03 +02:00 committed by Daniel Weiße
parent 9159b60331
commit d67d0ac9df
27 changed files with 782 additions and 531 deletions

View file

@ -8,6 +8,7 @@ package client
import (
"context"
"encoding/json"
"errors"
"fmt"
"path"
@ -15,9 +16,6 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go/aws"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/api/client"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
@ -33,153 +31,335 @@ func reportVersionDir(attestation variant.Variant) string {
return path.Join(attestationconfigapi.AttestationURLPath, attestation.String(), cachedVersionsSubDir)
}
// UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest
// 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) UploadSEVSNPVersionLatest(ctx context.Context, attestation variant.Variant, inputVersion,
latestAPIVersion attestationconfigapi.SEVSNPVersion, now time.Time, force bool,
func (c Client) UploadLatestVersion(
ctx context.Context, attestationVariant variant.Variant,
inputVersion, latestVersionInAPI any,
now time.Time, force bool,
) error {
if err := c.cacheSEVSNPVersion(ctx, attestation, inputVersion, now); err != nil {
return fmt.Errorf("reporting version: %w", err)
}
if force {
return c.uploadSEVSNPVersion(ctx, attestation, inputVersion, now)
}
versionDates, err := c.listCachedVersions(ctx, attestation)
// 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 fmt.Errorf("list reported versions: %w", err)
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.s3Client.Logger.Warn(fmt.Sprintf("Skipping version update, found %d, expected %d reported versions.", 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, attestation, versionDates)
minVersion, minDate, err := c.findMinVersion(ctx, attestationVariant, versionDates)
if err != nil {
return fmt.Errorf("get minimal version: %w", err)
return fmt.Errorf("determining minimal version in cache: %w", err)
}
c.s3Client.Logger.Info(fmt.Sprintf("Found minimal version: %+v with date: %s", minVersion, minDate))
shouldUpdateAPI, err := isInputNewerThanOtherVersion(minVersion, latestAPIVersion)
if err != nil {
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
}
if !shouldUpdateAPI {
c.s3Client.Logger.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v", minVersion, latestAPIVersion))
return nil
}
c.s3Client.Logger.Info(fmt.Sprintf("Input version: %+v is newer than latest API version: %+v", minVersion, latestAPIVersion))
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.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil {
return fmt.Errorf("uploading version: %w", err)
if err := c.uploadAsLatestVersion(ctx, attestationVariant, minVersion, t); err != nil {
return fmt.Errorf("uploading as latest version: %w", err)
}
c.s3Client.Logger.Info(fmt.Sprintf("Successfully uploaded new SEV-SNP version: %+v", minVersion))
c.log.Info(fmt.Sprintf("Successfully uploaded new %s version: %+v", attestationVariant, minVersion))
return nil
}
// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API.
func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version attestationconfigapi.SEVSNPVersion, date time.Time) error {
// 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"
res := putCmd{
apiObject: reportedSEVSNPVersionAPI{Version: dateStr, variant: attestation, SEVSNPVersion: version},
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 res.Execute(ctx, c.s3Client)
return cmd.Execute(ctx, c.s3Client)
}
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)
if strings.HasSuffix(fileName, ".json") {
dates = append(dates, fileName[:len(fileName)-5])
}
}
return dates, nil
// 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()
}
// findMinVersion finds the minimal version of the given version dates among the latest values in the version window size.
func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant, versionDates []string) (attestationconfigapi.SEVSNPVersion, string, error) {
var minimalVersion *attestationconfigapi.SEVSNPVersion
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[:c.cacheWindowSize]
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, c.s3Client, reportedSEVSNPVersionAPI{Version: date + ".json", variant: attesation})
obj, err := client.Fetch(ctx, s3Client, apiVersionObject{version: date + ".json", variant: variant, cached: true})
if err != nil {
return attestationconfigapi.SEVSNPVersion{}, "", fmt.Errorf("get object: %w", err)
return *new(T), "", fmt.Errorf("get object: %w", err)
}
// Need to set this explicitly as the variant is not part of the marshalled JSON.
obj.variant = attesation
obj.variant = variant // variant is not set by Fetch, set it manually
if minimalVersion == nil {
minimalVersion = &obj.SEVSNPVersion
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
} else {
shouldUpdateMinimal, err := isInputNewerThanOtherVersion(*minimalVersion, obj.SEVSNPVersion)
if err != nil {
continue
}
if shouldUpdateMinimal {
minimalVersion = &obj.SEVSNPVersion
minimalDate = date
}
}
}
return *minimalVersion, minimalDate, nil
}
// isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer.
func isInputNewerThanOtherVersion(input, other attestationconfigapi.SEVSNPVersion) (bool, error) {
if input == other {
return false, nil
}
if input.TEE < other.TEE {
return false, fmt.Errorf("input TEE version: %d is older than latest API version: %d", input.TEE, other.TEE)
}
if input.SNP < other.SNP {
return false, fmt.Errorf("input SNP version: %d is older than latest API version: %d", input.SNP, other.SNP)
}
if input.Microcode < other.Microcode {
return false, fmt.Errorf("input Microcode version: %d is older than latest API version: %d", input.Microcode, other.Microcode)
}
if input.Bootloader < other.Bootloader {
return false, fmt.Errorf("input Bootloader version: %d is older than latest API version: %d", input.Bootloader, other.Bootloader)
}
return true, nil
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
}
// reportedSEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
type reportedSEVSNPVersionAPI struct {
Version string `json:"-"`
type apiVersionObject struct {
version string `json:"-"`
variant variant.Variant `json:"-"`
attestationconfigapi.SEVSNPVersion
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.
func (i reportedSEVSNPVersionAPI) JSONPath() string {
return path.Join(reportVersionDir(i.variant), i.Version)
// 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 (i reportedSEVSNPVersionAPI) ValidateRequest() error {
if !strings.HasSuffix(i.Version, ".json") {
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 at the moment.
func (i reportedSEVSNPVersionAPI) Validate() error {
// 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
}