api: refactor attestationconfigapi client/fetcher

There is now one SEVSNPVersions type that has a variant
property. That property is used to build the correct JSON
path. The surrounding methods handling the version objects
are also updated to receive a variant argument and work
for multiple variants. This simplifies adding AWS support.
This commit is contained in:
Otto Bittner 2023-11-14 10:03:01 +01:00
parent 5542f9c63c
commit 350397923f
16 changed files with 411 additions and 262 deletions

View file

@ -17,6 +17,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
@ -294,25 +295,23 @@ func TestConfigFetchMeasurements(t *testing.T) {
type stubAttestationFetcher struct{} type stubAttestationFetcher struct{}
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionList) (attestationconfigapi.AzureSEVSNPVersionList, error) { func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) {
return attestationconfigapi.AzureSEVSNPVersionList( return attestationconfigapi.SEVSNPVersionList{}, nil
[]string{},
), nil
} }
func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionAPI) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { func (f stubAttestationFetcher) FetchSEVSNPVersion(_ context.Context, _ attestationconfigapi.SEVSNPVersionAPI) (attestationconfigapi.SEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{ return attestationconfigapi.SEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, SEVSNPVersion: testCfg,
}, nil }, nil
} }
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { func (f stubAttestationFetcher) FetchSEVSNPVersionLatest(_ context.Context, _ variant.Variant) (attestationconfigapi.SEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{ return attestationconfigapi.SEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, SEVSNPVersion: testCfg,
}, nil }, nil
} }
var testCfg = attestationconfigapi.AzureSEVSNPVersion{ var testCfg = attestationconfigapi.SEVSNPVersion{
Microcode: 93, Microcode: 93,
TEE: 0, TEE: 0,
SNP: 6, SNP: 6,

View file

@ -14,6 +14,7 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
@ -170,14 +171,14 @@ type stubConfigFetcher struct {
fetchLatestErr error fetchLatestErr error
} }
func (s *stubConfigFetcher) FetchAzureSEVSNPVersion(context.Context, attestationconfigapi.AzureSEVSNPVersionAPI) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { func (s *stubConfigFetcher) FetchSEVSNPVersion(context.Context, attestationconfigapi.SEVSNPVersionAPI) (attestationconfigapi.SEVSNPVersionAPI, error) {
panic("not implemented") panic("not implemented")
} }
func (s *stubConfigFetcher) FetchAzureSEVSNPVersionList(context.Context, attestationconfigapi.AzureSEVSNPVersionList) (attestationconfigapi.AzureSEVSNPVersionList, error) { func (s *stubConfigFetcher) FetchSEVSNPVersionList(context.Context, attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) {
panic("not implemented") panic("not implemented")
} }
func (s *stubConfigFetcher) FetchAzureSEVSNPVersionLatest(context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { func (s *stubConfigFetcher) FetchSEVSNPVersionLatest(context.Context, variant.Variant) (attestationconfigapi.SEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{}, s.fetchLatestErr return attestationconfigapi.SEVSNPVersionAPI{}, s.fetchLatestErr
} }

View file

@ -5,10 +5,10 @@ go_library(
name = "attestationconfigapi", name = "attestationconfigapi",
srcs = [ srcs = [
"attestationconfigapi.go", "attestationconfigapi.go",
"azure.go",
"client.go", "client.go",
"fetcher.go", "fetcher.go",
"reporter.go", "reporter.go",
"snp.go",
], ],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi", importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi",
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
@ -31,10 +31,13 @@ go_test(
"client_test.go", "client_test.go",
"fetcher_test.go", "fetcher_test.go",
"reporter_test.go", "reporter_test.go",
"snp_test.go",
], ],
embed = [":attestationconfigapi"], embed = [":attestationconfigapi"],
deps = [ deps = [
"//internal/attestation/variant",
"//internal/constants", "//internal/constants",
"@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
], ],
) )

View file

@ -1,84 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package attestationconfigapi
import (
"fmt"
"path"
"sort"
"strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
)
// attestationURLPath is the URL path to the attestation versions.
const attestationURLPath = "constellation/v1/attestation"
// AzureSEVSNPVersionType is the type of the version to be requested.
type AzureSEVSNPVersionType string
// AzureSEVSNPVersion tracks the latest version of each component of the Azure SEVSNP.
type AzureSEVSNPVersion struct {
// Bootloader is the latest version of the Azure SEVSNP bootloader.
Bootloader uint8 `json:"bootloader"`
// TEE is the latest version of the Azure SEVSNP TEE.
TEE uint8 `json:"tee"`
// SNP is the latest version of the Azure SEVSNP SNP.
SNP uint8 `json:"snp"`
// Microcode is the latest version of the Azure SEVSNP microcode.
Microcode uint8 `json:"microcode"`
}
// AzureSEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
type AzureSEVSNPVersionAPI struct {
Version string `json:"-"`
AzureSEVSNPVersion
}
// JSONPath returns the path to the JSON file for the request to the config api.
func (i AzureSEVSNPVersionAPI) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), i.Version)
}
// ValidateRequest validates the request.
func (i AzureSEVSNPVersionAPI) ValidateRequest() error {
if !strings.HasSuffix(i.Version, ".json") {
return fmt.Errorf("version has no .json suffix")
}
return nil
}
// Validate is a No-Op at the moment.
func (i AzureSEVSNPVersionAPI) Validate() error {
return nil
}
// AzureSEVSNPVersionList is the request to list all versions in the config api.
type AzureSEVSNPVersionList []string
// JSONPath returns the path to the JSON file for the request to the config api.
func (i AzureSEVSNPVersionList) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), "list")
}
// ValidateRequest is a NoOp as there is no input.
func (i AzureSEVSNPVersionList) ValidateRequest() error {
return nil
}
// SortAzureSEVSNPVersionList sorts the list of versions in reverse order.
func SortAzureSEVSNPVersionList(versions AzureSEVSNPVersionList) {
sort.Sort(sort.Reverse(sort.StringSlice(versions)))
}
// Validate validates the response.
func (i AzureSEVSNPVersionList) Validate() error {
if len(i) < 1 {
return fmt.Errorf("no versions found in /list")
}
return nil
}

View file

@ -15,13 +15,13 @@ go_library(
"azure.go", "azure.go",
"delete.go", "delete.go",
"main.go", "main.go",
"objectkind_string.go",
"validargs.go", "validargs.go",
], ],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli", importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli",
visibility = ["//visibility:private"], visibility = ["//visibility:private"],
deps = [ deps = [
"//internal/api/attestationconfigapi", "//internal/api/attestationconfigapi",
"//internal/attestation/variant",
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/constants", "//internal/constants",
"//internal/file", "//internal/file",
@ -44,11 +44,6 @@ go_test(
"main_test.go", "main_test.go",
], ],
embed = [":cli_lib"], embed = [":cli_lib"],
deps = [
"//internal/cloud/cloudprovider",
"//internal/verify",
"@com_github_stretchr_testify//assert",
],
) )
sh_template( sh_template(

View file

@ -15,6 +15,7 @@ import (
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
@ -36,7 +37,7 @@ func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg u
inputVersion := convertTCBVersionToAzureVersion(report.SNPReport.LaunchTCB) inputVersion := convertTCBVersionToAzureVersion(report.SNPReport.LaunchTCB)
log.Infof("Input report: %+v", inputVersion) log.Infof("Input report: %+v", inputVersion)
latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchAzureSEVSNPVersionLatest(ctx) latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{})
if err != nil { if err != nil {
if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) { if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) {
log.Infof("No versions found in API, but assuming that we are uploading the first version.") log.Infof("No versions found in API, but assuming that we are uploading the first version.")
@ -44,8 +45,8 @@ func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg u
return fmt.Errorf("fetching latest version: %w", err) return fmt.Errorf("fetching latest version: %w", err)
} }
} }
latestAPIVersion := latestAPIVersionAPI.AzureSEVSNPVersion latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion
if err := client.UploadAzureSEVSNPVersionLatest(ctx, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil { if err := client.UploadSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil {
if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) { if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) {
log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion) log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion)
return nil return nil
@ -56,8 +57,8 @@ func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg u
return nil return nil
} }
func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.AzureSEVSNPVersion { func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.SEVSNPVersion {
return attestationconfigapi.AzureSEVSNPVersion{ return attestationconfigapi.SEVSNPVersion{
Bootloader: tcb.Bootloader, Bootloader: tcb.Bootloader,
TEE: tcb.TEE, TEE: tcb.TEE,
SNP: tcb.SNP, SNP: tcb.SNP,
@ -67,7 +68,7 @@ func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi
func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error {
if cfg.provider == cloudprovider.Azure && cfg.kind == snpReport { if cfg.provider == cloudprovider.Azure && cfg.kind == snpReport {
return client.DeleteAzureSEVSNPVersion(ctx, cfg.version) return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version)
} }
return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind)

View file

@ -48,24 +48,25 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK
return repo, clientClose, nil return repo, clientClose, nil
} }
// uploadAzureSEVSNPVersion 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. // uploadSEVSNPVersion 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) uploadAzureSEVSNPVersion(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error { func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error {
versions, err := a.List(ctx, variant.AzureSEVSNP{}) versions, err := a.List(ctx, attestation)
if err != nil { if err != nil {
return fmt.Errorf("fetch version list: %w", err) return fmt.Errorf("fetch version list: %w", err)
} }
ops := a.constructUploadCmd(version, versions, date) ops := a.constructUploadCmd(attestation, version, versions, date)
return executeAllCmds(ctx, a.s3Client, ops) return executeAllCmds(ctx, a.s3Client, ops)
} }
// DeleteAzureSEVSNPVersion deletes the given version (without .json suffix) from the API. // DeleteSEVSNPVersion deletes the given version (without .json suffix) from the API.
func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error { func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Variant, versionStr string) error {
versions, err := a.List(ctx, variant.AzureSEVSNP{}) versions, err := a.List(ctx, attestation)
if err != nil { if err != nil {
return fmt.Errorf("fetch version list: %w", err) return fmt.Errorf("fetch version list: %w", err)
} }
ops, err := a.deleteAzureSEVSNPVersion(versions, versionStr)
ops, err := a.deleteSEVSNPVersion(versions, versionStr)
if err != nil { if err != nil {
return err return err
} }
@ -73,25 +74,30 @@ func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string)
} }
// List returns the list of versions for the given attestation variant. // List returns the list of versions for the given attestation variant.
func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) { func (a Client) List(ctx context.Context, attestation variant.Variant) (SEVSNPVersionList, error) {
if attestation.Equal(variant.AzureSEVSNP{}) { if !attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{}) {
versions, err := apiclient.Fetch(ctx, a.s3Client, AzureSEVSNPVersionList{}) return SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation)
}
versions, err := apiclient.Fetch(ctx, a.s3Client, SEVSNPVersionList{variant: attestation})
if err != nil { if err != nil {
var notFoundErr *apiclient.NotFoundError var notFoundErr *apiclient.NotFoundError
if errors.As(err, &notFoundErr) { if errors.As(err, &notFoundErr) {
return nil, nil return SEVSNPVersionList{variant: attestation}, nil
} }
return nil, err return SEVSNPVersionList{}, 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) { versions.variant = attestation
return versions, nil
}
func (a Client) deleteSEVSNPVersion(versions SEVSNPVersionList, versionStr string) (ops []crudCmd, err error) {
versionStr = versionStr + ".json" versionStr = versionStr + ".json"
ops = append(ops, deleteCmd{ ops = append(ops, deleteCmd{
apiObject: AzureSEVSNPVersionAPI{ apiObject: SEVSNPVersionAPI{
Variant: versions.variant,
Version: versionStr, Version: versionStr,
}, },
}) })
@ -107,36 +113,42 @@ func (a Client) deleteAzureSEVSNPVersion(versions AzureSEVSNPVersionList, versio
return ops, nil return ops, nil
} }
func (a Client) constructUploadCmd(versions AzureSEVSNPVersion, versionNames []string, date time.Time) []crudCmd { func (a Client) constructUploadCmd(attestation variant.Variant, version SEVSNPVersion, versionNames SEVSNPVersionList, date time.Time) []crudCmd {
if !attestation.Equal(versionNames.variant) {
return nil
}
dateStr := date.Format(VersionFormat) + ".json" dateStr := date.Format(VersionFormat) + ".json"
var res []crudCmd var res []crudCmd
res = append(res, putCmd{ res = append(res, putCmd{
apiObject: AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: versions}, apiObject: SEVSNPVersionAPI{Version: dateStr, Variant: attestation, SEVSNPVersion: version},
signer: a.signer, signer: a.signer,
}) })
newVersions := addVersion(versionNames, dateStr) versionNames.addVersion(dateStr)
res = append(res, putCmd{ res = append(res, putCmd{
apiObject: AzureSEVSNPVersionList(newVersions), apiObject: versionNames,
signer: a.signer, signer: a.signer,
}) })
return res return res
} }
func removeVersion(versions AzureSEVSNPVersionList, versionStr string) (removedVersions AzureSEVSNPVersionList, err error) { func removeVersion(list SEVSNPVersionList, versionStr string) (removedVersions SEVSNPVersionList, err error) {
versions := list.List()
for i, v := range versions { for i, v := range versions {
if v == versionStr { if v == versionStr {
if i == len(versions)-1 { if i == len(versions)-1 {
removedVersions = versions[:i] removedVersions = SEVSNPVersionList{list: versions[:i], variant: list.variant}
} else { } else {
removedVersions = append(versions[:i], versions[i+1:]...) removedVersions = SEVSNPVersionList{list: append(versions[:i], versions[i+1:]...), variant: list.variant}
} }
return removedVersions, nil return removedVersions, nil
} }
} }
return nil, fmt.Errorf("version %s not found in list %v", versionStr, versions) return SEVSNPVersionList{}, fmt.Errorf("version %s not found in list %v", versionStr, versions)
} }
type crudCmd interface { type crudCmd interface {
@ -168,10 +180,3 @@ func executeAllCmds(ctx context.Context, client *apiclient.Client, cmds []crudCm
} }
return nil return nil
} }
func addVersion(versions []string, newVersion string) []string {
versions = append(versions, newVersion)
versions = variant.RemoveDuplicate(versions)
SortAzureSEVSNPVersionList(versions)
return versions
}

View file

@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -17,20 +18,21 @@ func TestUploadAzureSEVSNP(t *testing.T) {
bucketID: "bucket", bucketID: "bucket",
signer: fakeSigner{}, signer: fakeSigner{},
} }
version := AzureSEVSNPVersion{} version := SEVSNPVersion{}
date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC) date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
ops := sut.constructUploadCmd(version, []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, date) ops := sut.constructUploadCmd(variant.AzureSEVSNP{}, version, SEVSNPVersionList{list: []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, variant: variant.AzureSEVSNP{}}, date)
dateStr := "2023-01-01-01-01.json" dateStr := "2023-01-01-01-01.json"
assert := assert.New(t) assert := assert.New(t)
assert.Contains(ops, putCmd{ assert.Contains(ops, putCmd{
apiObject: AzureSEVSNPVersionAPI{ apiObject: SEVSNPVersionAPI{
Variant: variant.AzureSEVSNP{},
Version: dateStr, Version: dateStr,
AzureSEVSNPVersion: version, SEVSNPVersion: version,
}, },
signer: fakeSigner{}, signer: fakeSigner{},
}) })
assert.Contains(ops, putCmd{ assert.Contains(ops, putCmd{
apiObject: AzureSEVSNPVersionList([]string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}), apiObject: SEVSNPVersionList{variant: variant.AzureSEVSNP{}, list: []string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}},
signer: fakeSigner{}, signer: fakeSigner{},
}) })
} }
@ -39,20 +41,20 @@ func TestDeleteAzureSEVSNPVersions(t *testing.T) {
sut := Client{ sut := Client{
bucketID: "bucket", bucketID: "bucket",
} }
versions := AzureSEVSNPVersionList([]string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}) versions := SEVSNPVersionList{list: []string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}}
ops, err := sut.deleteAzureSEVSNPVersion(versions, "2021-01-01") ops, err := sut.deleteSEVSNPVersion(versions, "2021-01-01")
assert := assert.New(t) assert := assert.New(t)
assert.NoError(err) assert.NoError(err)
assert.Contains(ops, deleteCmd{ assert.Contains(ops, deleteCmd{
apiObject: AzureSEVSNPVersionAPI{ apiObject: SEVSNPVersionAPI{
Version: "2021-01-01.json", Version: "2021-01-01.json",
}, },
}) })
assert.Contains(ops, putCmd{ assert.Contains(ops, putCmd{
apiObject: AzureSEVSNPVersionList([]string{"2023-01-01.json", "2019-01-01.json"}), apiObject: SEVSNPVersionList{list: []string{"2023-01-01.json", "2019-01-01.json"}},
}) })
} }

View file

@ -12,6 +12,7 @@ import (
"fmt" "fmt"
apifetcher "github.com/edgelesssys/constellation/v2/internal/api/fetcher" apifetcher "github.com/edgelesssys/constellation/v2/internal/api/fetcher"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/sigstore"
) )
@ -23,9 +24,9 @@ var ErrNoVersionsFound = errors.New("no versions found")
// Fetcher fetches config API resources without authentication. // Fetcher fetches config API resources without authentication.
type Fetcher interface { type Fetcher interface {
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error) FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionAPI) (SEVSNPVersionAPI, error)
FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, error) FetchSEVSNPVersionList(ctx context.Context, list SEVSNPVersionList) (SEVSNPVersionList, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context) (AzureSEVSNPVersionAPI, error) FetchSEVSNPVersionLatest(ctx context.Context, attesation variant.Variant) (SEVSNPVersionAPI, error)
} }
// fetcher fetches AttestationCfg API resources without authentication. // fetcher fetches AttestationCfg API resources without authentication.
@ -64,35 +65,45 @@ func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifie
return &fetcher{HTTPClient: client, verifier: cosignVerifier, cdnURL: url} return &fetcher{HTTPClient: client, verifier: cosignVerifier, cdnURL: url}
} }
// FetchAzureSEVSNPVersionList fetches the version list information from the config API. // FetchSEVSNPVersionList fetches the version list information from the config API.
func (f *fetcher) FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, error) { func (f *fetcher) FetchSEVSNPVersionList(ctx context.Context, list SEVSNPVersionList) (SEVSNPVersionList, error) {
// TODO(derpsteb): Replace with FetchAndVerify once we move to v2 of the config API. // TODO(derpsteb): Replace with FetchAndVerify once we move to v2 of the config API.
return apifetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, attestation) fetchedList, err := apifetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, list)
if err != nil {
return list, fmt.Errorf("fetching version list: %w", err)
} }
// FetchAzureSEVSNPVersion fetches the version information from the config API. // Need to set this explicitly as the variant is not part of the marshalled JSON.
func (f *fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error) { fetchedList.variant = list.variant
fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, azureVersion, f.verifier)
if err != nil { return fetchedList, nil
return fetchedVersion, fmt.Errorf("fetching version %s: %w", azureVersion.Version, err)
} }
// FetchSEVSNPVersion fetches the version information from the config API.
func (f *fetcher) FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionAPI) (SEVSNPVersionAPI, error) {
fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, version, f.verifier)
if err != nil {
return fetchedVersion, fmt.Errorf("fetching version %s: %w", version.Version, err)
}
// Need to set this explicitly as the variant is not part of the marshalled JSON.
fetchedVersion.Variant = version.Variant
return fetchedVersion, nil return fetchedVersion, nil
} }
// FetchAzureSEVSNPVersionLatest returns the latest versions of the given type. // FetchSEVSNPVersionLatest returns the latest versions of the given type.
func (f *fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res AzureSEVSNPVersionAPI, err error) { func (f *fetcher) FetchSEVSNPVersionLatest(ctx context.Context, attesation variant.Variant) (res SEVSNPVersionAPI, err error) {
var list AzureSEVSNPVersionList list, err := f.FetchSEVSNPVersionList(ctx, SEVSNPVersionList{variant: attesation})
list, err = f.FetchAzureSEVSNPVersionList(ctx, list)
if err != nil { if err != nil {
return res, ErrNoVersionsFound return res, ErrNoVersionsFound
} }
if len(list) < 1 {
return res, ErrNoVersionsFound getVersionRequest := SEVSNPVersionAPI{
Version: list.List()[0], // latest version is first in list
Variant: attesation,
} }
getVersionRequest := AzureSEVSNPVersionAPI{ res, err = f.FetchSEVSNPVersion(ctx, getVersionRequest)
Version: list[0], // latest version is first in list
}
res, err = f.FetchAzureSEVSNPVersion(ctx, getVersionRequest)
if err != nil { if err != nil {
return res, err return res, err
} }

View file

@ -16,48 +16,65 @@ import (
"testing" "testing"
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestFetchLatestAzureSEVSNPVersion(t *testing.T) { func TestFetchLatestSEVSNPVersion(t *testing.T) {
latestStr := "2023-06-11-14-09.json" latestStr := "2023-06-11-14-09.json"
olderStr := "2019-01-01-01-01.json" olderStr := "2019-01-01-01-01.json"
testcases := map[string]struct { testcases := map[string]struct {
fetcherVersions []string fetcherVersions []string
timeAtTest time.Time timeAtTest time.Time
wantErr bool wantErr bool
want AzureSEVSNPVersionAPI attestation variant.Variant
expectedVersion func() SEVSNPVersionAPI
olderVersion func() SEVSNPVersionAPI
latestVersion func() SEVSNPVersionAPI
}{ }{
"get latest version": { "get latest version azure": {
fetcherVersions: []string{latestStr, olderStr}, fetcherVersions: []string{latestStr, olderStr},
want: latestVersion, attestation: variant.AzureSEVSNP{},
expectedVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AzureSEVSNP{}; return tmp },
olderVersion: func() SEVSNPVersionAPI { tmp := olderVersion; tmp.Variant = variant.AzureSEVSNP{}; return tmp },
latestVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AzureSEVSNP{}; return tmp },
},
"get latest version aws": {
fetcherVersions: []string{latestStr, olderStr},
attestation: variant.AWSSEVSNP{},
expectedVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AWSSEVSNP{}; return tmp },
olderVersion: func() SEVSNPVersionAPI { tmp := olderVersion; tmp.Variant = variant.AWSSEVSNP{}; return tmp },
latestVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AWSSEVSNP{}; return tmp },
}, },
} }
for name, tc := range testcases { for name, tc := range testcases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
client := &http.Client{ client := &http.Client{
Transport: &fakeConfigAPIHandler{ Transport: &fakeConfigAPIHandler{
attestation: tc.attestation,
versions: tc.fetcherVersions, versions: tc.fetcherVersions,
latestVersion: latestStr, latestDate: latestStr,
olderVersion: olderStr, latestVersion: tc.latestVersion(),
olderDate: olderStr,
olderVersion: tc.olderVersion(),
}, },
} }
fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{}, constants.CDNRepositoryURL) fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{}, constants.CDNRepositoryURL)
res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background()) res, err := fetcher.FetchSEVSNPVersionLatest(context.Background(), tc.attestation)
assert := assert.New(t) assert := assert.New(t)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { } else {
assert.NoError(err) assert.NoError(err)
assert.Equal(tc.want, res) assert.Equal(tc.expectedVersion(), res)
} }
}) })
} }
} }
var latestVersion = AzureSEVSNPVersionAPI{ var latestVersion = SEVSNPVersionAPI{
AzureSEVSNPVersion: AzureSEVSNPVersion{ SEVSNPVersion: SEVSNPVersion{
Microcode: 93, Microcode: 93,
TEE: 0, TEE: 0,
SNP: 6, SNP: 6,
@ -65,8 +82,8 @@ var latestVersion = AzureSEVSNPVersionAPI{
}, },
} }
var olderVersion = AzureSEVSNPVersionAPI{ var olderVersion = SEVSNPVersionAPI{
AzureSEVSNPVersion: AzureSEVSNPVersion{ SEVSNPVersion: SEVSNPVersion{
Microcode: 1, Microcode: 1,
TEE: 0, TEE: 0,
SNP: 1, SNP: 1,
@ -75,14 +92,17 @@ var olderVersion = AzureSEVSNPVersionAPI{
} }
type fakeConfigAPIHandler struct { type fakeConfigAPIHandler struct {
attestation variant.Variant
versions []string versions []string
latestVersion string latestDate string
olderVersion string latestVersion SEVSNPVersionAPI
olderDate string
olderVersion SEVSNPVersionAPI
} }
// RoundTrip resolves the request and returns a dummy response. // RoundTrip resolves the request and returns a dummy response.
func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, error) { func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/list" { if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/list", f.attestation.String()) {
res := &http.Response{} res := &http.Response{}
bt, err := json.Marshal(f.versions) bt, err := json.Marshal(f.versions)
if err != nil { if err != nil {
@ -93,9 +113,9 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err
res.Header.Set("Content-Type", "application/json") res.Header.Set("Content-Type", "application/json")
res.StatusCode = http.StatusOK res.StatusCode = http.StatusOK
return res, nil return res, nil
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s", f.latestVersion) { } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.latestDate) {
res := &http.Response{} res := &http.Response{}
bt, err := json.Marshal(latestVersion) bt, err := json.Marshal(f.latestVersion)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -103,22 +123,22 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err
res.StatusCode = http.StatusOK res.StatusCode = http.StatusOK
return res, nil return res, nil
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s", f.olderVersion) { } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.olderDate) {
res := &http.Response{} res := &http.Response{}
bt, err := json.Marshal(olderVersion) bt, err := json.Marshal(f.olderVersion)
if err != nil { if err != nil {
return nil, err return nil, err
} }
res.Body = io.NopCloser(bytes.NewReader(bt)) res.Body = io.NopCloser(bytes.NewReader(bt))
res.StatusCode = http.StatusOK res.StatusCode = http.StatusOK
return res, nil return res, nil
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s.sig", f.latestVersion) { } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.latestDate) {
res := &http.Response{} res := &http.Response{}
res.Body = io.NopCloser(bytes.NewReader([]byte("null"))) res.Body = io.NopCloser(bytes.NewReader([]byte("null")))
res.StatusCode = http.StatusOK res.StatusCode = http.StatusOK
return res, nil return res, nil
} else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s.sig", f.olderVersion) { } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.olderDate) {
res := &http.Response{} res := &http.Response{}
res.Body = io.NopCloser(bytes.NewReader([]byte("null"))) res.Body = io.NopCloser(bytes.NewReader([]byte("null")))
res.StatusCode = http.StatusOK res.StatusCode = http.StatusOK

View file

@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
/* /*
The reporter contains the logic to determine a latest version for Azure SEVSNP based on cached version values observed on CVM instances. The reporter contains the logic to determine a latest version for Azure SEVSNP based on cached version values observed on CVM instances.
Some code in this file (e.g. listing cached files) does not rely on dedicated API objects and instead uses the AWS SDK directly,
for no other reason than original development speed.
*/ */
package attestationconfigapi package attestationconfigapi
@ -28,25 +30,27 @@ import (
// cachedVersionsSubDir is the subdirectory in the bucket where the cached versions are stored. // cachedVersionsSubDir is the subdirectory in the bucket where the cached versions are stored.
const cachedVersionsSubDir = "cached-versions" const cachedVersionsSubDir = "cached-versions"
var reportVersionDir = path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), cachedVersionsSubDir)
// ErrNoNewerVersion is returned if the input version is not newer than the latest API version. // 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") var ErrNoNewerVersion = errors.New("input version is not newer than latest API version")
// UploadAzureSEVSNPVersionLatest saves the given version to the cache, determines the smallest func reportVersionDir(attestation variant.Variant) string {
return path.Join(attestationURLPath, attestation.String(), cachedVersionsSubDir)
}
// UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest
// TCB version in the cache among the last cacheWindowSize versions and updates // TCB version in the cache among the last cacheWindowSize versions and updates
// the latest version in the API if there is an update. // the latest version in the API if there is an update.
// force can be used to bypass the validation logic against the cached versions. // force can be used to bypass the validation logic against the cached versions.
func (c Client) UploadAzureSEVSNPVersionLatest(ctx context.Context, inputVersion, func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation variant.Variant, inputVersion,
latestAPIVersion AzureSEVSNPVersion, now time.Time, force bool, latestAPIVersion SEVSNPVersion, now time.Time, force bool,
) error { ) error {
if err := c.cacheAzureSEVSNPVersion(ctx, inputVersion, now); err != nil { if err := c.cacheSEVSNPVersion(ctx, attestation, inputVersion, now); err != nil {
return fmt.Errorf("reporting version: %w", err) return fmt.Errorf("reporting version: %w", err)
} }
if force { if force {
return c.uploadAzureSEVSNPVersion(ctx, inputVersion, now) return c.uploadSEVSNPVersion(ctx, attestation, inputVersion, now)
} }
versionDates, err := c.listCachedVersions(ctx) versionDates, err := c.listCachedVersions(ctx, attestation)
if err != nil { if err != nil {
return fmt.Errorf("list reported versions: %w", err) return fmt.Errorf("list reported versions: %w", err)
} }
@ -54,7 +58,7 @@ func (c Client) UploadAzureSEVSNPVersionLatest(ctx context.Context, inputVersion
c.s3Client.Logger.Warnf("Skipping version update, found %d, expected %d reported versions.", len(versionDates), c.cacheWindowSize) c.s3Client.Logger.Warnf("Skipping version update, found %d, expected %d reported versions.", len(versionDates), c.cacheWindowSize)
return nil return nil
} }
minVersion, minDate, err := c.findMinVersion(ctx, versionDates) minVersion, minDate, err := c.findMinVersion(ctx, attestation, versionDates)
if err != nil { if err != nil {
return fmt.Errorf("get minimal version: %w", err) return fmt.Errorf("get minimal version: %w", err)
} }
@ -72,27 +76,27 @@ func (c Client) UploadAzureSEVSNPVersionLatest(ctx context.Context, inputVersion
if err != nil { if err != nil {
return fmt.Errorf("parsing date: %w", err) return fmt.Errorf("parsing date: %w", err)
} }
if err := c.uploadAzureSEVSNPVersion(ctx, minVersion, t); err != nil { if err := c.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil {
return fmt.Errorf("uploading version: %w", err) return fmt.Errorf("uploading version: %w", err)
} }
c.s3Client.Logger.Infof("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion) c.s3Client.Logger.Infof("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion)
return nil return nil
} }
// cacheAzureSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API. // cacheSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API.
func (c Client) cacheAzureSEVSNPVersion(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error { func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error {
dateStr := date.Format(VersionFormat) + ".json" dateStr := date.Format(VersionFormat) + ".json"
res := putCmd{ res := putCmd{
apiObject: reportedAzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: version}, apiObject: reportedSEVSNPVersionAPI{Version: dateStr, variant: attestation, SEVSNPVersion: version},
signer: c.signer, signer: c.signer,
} }
return res.Execute(ctx, c.s3Client) return res.Execute(ctx, c.s3Client)
} }
func (c Client) listCachedVersions(ctx context.Context) ([]string, error) { func (c Client) listCachedVersions(ctx context.Context, attestation variant.Variant) ([]string, error) {
list, err := c.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ list, err := c.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(c.bucketID), Bucket: aws.String(c.bucketID),
Prefix: aws.String(reportVersionDir), Prefix: aws.String(reportVersionDir(attestation)),
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("list objects: %w", err) return nil, fmt.Errorf("list objects: %w", err)
@ -108,28 +112,30 @@ func (c Client) listCachedVersions(ctx context.Context) ([]string, error) {
} }
// findMinVersion finds the minimal version of the given version dates among the latest values in the version window size. // 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, versionDates []string) (AzureSEVSNPVersion, string, error) { func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant, versionDates []string) (SEVSNPVersion, string, error) {
var minimalVersion *AzureSEVSNPVersion var minimalVersion *SEVSNPVersion
var minimalDate string var minimalDate string
sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions
versionDates = versionDates[:c.cacheWindowSize] versionDates = versionDates[:c.cacheWindowSize]
sort.Strings(versionDates) // sort with oldest first to to take the minimal version with the oldest date sort.Strings(versionDates) // sort with oldest first to to take the minimal version with the oldest date
for _, date := range versionDates { for _, date := range versionDates {
obj, err := client.Fetch(ctx, c.s3Client, reportedAzureSEVSNPVersionAPI{Version: date + ".json"}) obj, err := client.Fetch(ctx, c.s3Client, reportedSEVSNPVersionAPI{Version: date + ".json", variant: attesation})
if err != nil { if err != nil {
return AzureSEVSNPVersion{}, "", fmt.Errorf("get object: %w", err) return SEVSNPVersion{}, "", fmt.Errorf("get object: %w", err)
} }
// Need to set this explicitly as the variant is not part of the marshalled JSON.
obj.variant = attesation
if minimalVersion == nil { if minimalVersion == nil {
minimalVersion = &obj.AzureSEVSNPVersion minimalVersion = &obj.SEVSNPVersion
minimalDate = date minimalDate = date
} else { } else {
shouldUpdateMinimal, err := isInputNewerThanOtherVersion(*minimalVersion, obj.AzureSEVSNPVersion) shouldUpdateMinimal, err := isInputNewerThanOtherVersion(*minimalVersion, obj.SEVSNPVersion)
if err != nil { if err != nil {
continue continue
} }
if shouldUpdateMinimal { if shouldUpdateMinimal {
minimalVersion = &obj.AzureSEVSNPVersion minimalVersion = &obj.SEVSNPVersion
minimalDate = date minimalDate = date
} }
} }
@ -138,7 +144,7 @@ func (c Client) findMinVersion(ctx context.Context, versionDates []string) (Azur
} }
// isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer. // isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer.
func isInputNewerThanOtherVersion(input, other AzureSEVSNPVersion) (bool, error) { func isInputNewerThanOtherVersion(input, other SEVSNPVersion) (bool, error) {
if input == other { if input == other {
return false, nil return false, nil
} }
@ -157,19 +163,20 @@ func isInputNewerThanOtherVersion(input, other AzureSEVSNPVersion) (bool, error)
return true, nil return true, nil
} }
// reportedAzureSEVSNPVersionAPI is the request to get the version information of the specific version in the config api. // reportedSEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
type reportedAzureSEVSNPVersionAPI struct { type reportedSEVSNPVersionAPI struct {
Version string `json:"-"` Version string `json:"-"`
AzureSEVSNPVersion variant variant.Variant `json:"-"`
SEVSNPVersion
} }
// JSONPath returns the path to the JSON file for the request to the config api. // JSONPath returns the path to the JSON file for the request to the config api.
func (i reportedAzureSEVSNPVersionAPI) JSONPath() string { func (i reportedSEVSNPVersionAPI) JSONPath() string {
return path.Join(reportVersionDir, i.Version) return path.Join(reportVersionDir(i.variant), i.Version)
} }
// ValidateRequest validates the request. // ValidateRequest validates the request.
func (i reportedAzureSEVSNPVersionAPI) ValidateRequest() error { func (i reportedSEVSNPVersionAPI) ValidateRequest() error {
if !strings.HasSuffix(i.Version, ".json") { if !strings.HasSuffix(i.Version, ".json") {
return fmt.Errorf("version has no .json suffix") return fmt.Errorf("version has no .json suffix")
} }
@ -177,6 +184,6 @@ func (i reportedAzureSEVSNPVersionAPI) ValidateRequest() error {
} }
// Validate is a No-Op at the moment. // Validate is a No-Op at the moment.
func (i reportedAzureSEVSNPVersionAPI) Validate() error { func (i reportedSEVSNPVersionAPI) Validate() error {
return nil return nil
} }

View file

@ -11,8 +11,8 @@ import (
) )
func TestIsInputNewerThanLatestAPI(t *testing.T) { func TestIsInputNewerThanLatestAPI(t *testing.T) {
newTestCfg := func() AzureSEVSNPVersion { newTestCfg := func() SEVSNPVersion {
return AzureSEVSNPVersion{ return SEVSNPVersion{
Microcode: 93, Microcode: 93,
TEE: 0, TEE: 0,
SNP: 6, SNP: 6,
@ -21,13 +21,13 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
latest AzureSEVSNPVersion latest SEVSNPVersion
input AzureSEVSNPVersion input SEVSNPVersion
expect bool expect bool
errMsg string errMsg string
}{ }{
"input is older than latest": { "input is older than latest": {
input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion { input: func(c SEVSNPVersion) SEVSNPVersion {
c.Microcode-- c.Microcode--
return c return c
}(newTestCfg()), }(newTestCfg()),
@ -36,7 +36,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
errMsg: "input Microcode version: 92 is older than latest API version: 93", errMsg: "input Microcode version: 92 is older than latest API version: 93",
}, },
"input has greater and smaller version field than latest": { "input has greater and smaller version field than latest": {
input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion { input: func(c SEVSNPVersion) SEVSNPVersion {
c.Microcode++ c.Microcode++
c.Bootloader-- c.Bootloader--
return c return c
@ -46,7 +46,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
errMsg: "input Bootloader version: 1 is older than latest API version: 2", errMsg: "input Bootloader version: 1 is older than latest API version: 2",
}, },
"input is newer than latest": { "input is newer than latest": {
input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion { input: func(c SEVSNPVersion) SEVSNPVersion {
c.TEE++ c.TEE++
return c return c
}(newTestCfg()), }(newTestCfg()),

View file

@ -0,0 +1,113 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package attestationconfigapi
import (
"encoding/json"
"fmt"
"path"
"sort"
"strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
)
// attestationURLPath is the URL path to the attestation versions.
const attestationURLPath = "constellation/v1/attestation"
// SEVSNPVersion tracks the latest version of each component of the Azure SEVSNP.
type SEVSNPVersion struct {
// Bootloader is the latest version of the Azure SEVSNP bootloader.
Bootloader uint8 `json:"bootloader"`
// TEE is the latest version of the Azure SEVSNP TEE.
TEE uint8 `json:"tee"`
// SNP is the latest version of the Azure SEVSNP SNP.
SNP uint8 `json:"snp"`
// Microcode is the latest version of the Azure SEVSNP microcode.
Microcode uint8 `json:"microcode"`
}
// SEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
// Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property.
// Once we switch to v2 of the API we should embed the variant in the object.
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
type SEVSNPVersionAPI struct {
Version string `json:"-"`
Variant variant.Variant `json:"-"`
SEVSNPVersion
}
// JSONPath returns the path to the JSON file for the request to the config api.
func (i SEVSNPVersionAPI) JSONPath() string {
return path.Join(attestationURLPath, i.Variant.String(), i.Version)
}
// ValidateRequest validates the request.
func (i SEVSNPVersionAPI) ValidateRequest() error {
if !strings.HasSuffix(i.Version, ".json") {
return fmt.Errorf("version has no .json suffix")
}
return nil
}
// Validate is a No-Op at the moment.
func (i SEVSNPVersionAPI) Validate() error {
return nil
}
// SEVSNPVersionList is the request to list all versions in the config api.
// Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property.
// Once we switch to v2 of the API we could embed the variant in the object and remove some code from fetcher & client.
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
type SEVSNPVersionList struct {
variant variant.Variant
list []string
}
// MarshalJSON marshals the i's list property to JSON.
func (i SEVSNPVersionList) MarshalJSON() ([]byte, error) {
return json.Marshal(i.list)
}
// UnmarshalJSON unmarshals a list of strings into i's list property.
func (i *SEVSNPVersionList) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &i.list)
}
// List returns i's list property.
func (i SEVSNPVersionList) List() []string { return i.list }
// JSONPath returns the path to the JSON file for the request to the config api.
func (i SEVSNPVersionList) JSONPath() string {
return path.Join(attestationURLPath, i.variant.String(), "list")
}
// ValidateRequest is a NoOp as there is no input.
func (i SEVSNPVersionList) ValidateRequest() error {
return nil
}
// SortReverse sorts the list of versions in reverse order.
func (i *SEVSNPVersionList) SortReverse() {
sort.Sort(sort.Reverse(sort.StringSlice(i.list)))
}
// addVersion adds new to i's list and sorts the element in descending order.
func (i *SEVSNPVersionList) addVersion(new string) {
i.list = append(i.list, new)
i.list = variant.RemoveDuplicate(i.list)
i.SortReverse()
}
// Validate validates the response.
func (i SEVSNPVersionList) Validate() error {
if len(i.list) < 1 {
return fmt.Errorf("no versions found in /list")
}
return nil
}

View file

@ -0,0 +1,77 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package attestationconfigapi
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSEVSNPVersionListMarshalUnmarshalJSON(t *testing.T) {
tests := map[string]struct {
input SEVSNPVersionList
output SEVSNPVersionList
wantDiff bool
}{
"success": {
input: SEVSNPVersionList{list: []string{"v1", "v2"}},
output: SEVSNPVersionList{list: []string{"v1", "v2"}},
},
"variant is lost": {
input: SEVSNPVersionList{list: []string{"v1", "v2"}, variant: variant.AzureSEVSNP{}},
output: SEVSNPVersionList{list: []string{"v1", "v2"}},
},
"wrong order": {
input: SEVSNPVersionList{list: []string{"v1", "v2"}},
output: SEVSNPVersionList{list: []string{"v2", "v1"}},
wantDiff: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
inputRaw, err := tc.input.MarshalJSON()
require.NoError(t, err)
var actual SEVSNPVersionList
err = actual.UnmarshalJSON(inputRaw)
require.NoError(t, err)
if tc.wantDiff {
assert.NotEqual(t, tc.output, actual, "Objects are equal, expected unequal")
} else {
assert.Equal(t, tc.output, actual, "Objects are not equal, expected equal")
}
})
}
}
func TestSEVSNPVersionListAddVersion(t *testing.T) {
tests := map[string]struct {
versions []string
new string
expected []string
}{
"success": {
versions: []string{"v1", "v2"},
new: "v3",
expected: []string{"v3", "v2", "v1"},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
v := SEVSNPVersionList{list: tc.versions}
v.addVersion(tc.new)
assert.Equal(t, tc.expected, v.list)
})
}
}

View file

@ -69,16 +69,16 @@ func (c AzureSEVSNP) EqualTo(old AttestationCfg) (bool, error) {
// FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. // FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them.
func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error { func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error {
versions, err := fetcher.FetchAzureSEVSNPVersionLatest(ctx) versions, err := fetcher.FetchSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{})
if err != nil { if err != nil {
return err return err
} }
// set number and keep isLatest flag // set number and keep isLatest flag
c.mergeWithLatestVersion(versions.AzureSEVSNPVersion) c.mergeWithLatestVersion(versions.SEVSNPVersion)
return nil return nil
} }
func (c *AzureSEVSNP) mergeWithLatestVersion(latest attestationconfigapi.AzureSEVSNPVersion) { func (c *AzureSEVSNP) mergeWithLatestVersion(latest attestationconfigapi.SEVSNPVersion) {
if c.BootloaderVersion.WantLatest { if c.BootloaderVersion.WantLatest {
c.BootloaderVersion.Value = latest.Bootloader c.BootloaderVersion.Value = latest.Bootloader
} }

View file

@ -23,6 +23,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes" "github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
@ -51,10 +52,10 @@ func TestDefaultConfigMarshalsLatestVersion(t *testing.T) {
var mp configMap var mp configMap
require.NoError(yaml.Unmarshal(bt, &mp)) require.NoError(yaml.Unmarshal(bt, &mp))
assert := assert.New(t) assert := assert.New(t)
assert.Equal("latest", mp.getAzureSEVSNPVersion("microcodeVersion")) assert.Equal("latest", mp.getSEVSNPVersion("microcodeVersion"))
assert.Equal("latest", mp.getAzureSEVSNPVersion("teeVersion")) assert.Equal("latest", mp.getSEVSNPVersion("teeVersion"))
assert.Equal("latest", mp.getAzureSEVSNPVersion("snpVersion")) assert.Equal("latest", mp.getSEVSNPVersion("snpVersion"))
assert.Equal("latest", mp.getAzureSEVSNPVersion("bootloaderVersion")) assert.Equal("latest", mp.getSEVSNPVersion("bootloaderVersion"))
} }
func TestGetAttestationConfigMarshalsNumericalVersion(t *testing.T) { func TestGetAttestationConfigMarshalsNumericalVersion(t *testing.T) {
@ -88,9 +89,9 @@ func TestNew(t *testing.T) {
conf := Default() // default configures latest version conf := Default() // default configures latest version
modifyConfigForAzureToPassValidate(conf) modifyConfigForAzureToPassValidate(conf)
m := getConfigAsMap(conf, t) m := getConfigAsMap(conf, t)
m.setAzureSEVSNPVersion("microcodeVersion", "Latest") // check uppercase also works m.setSEVSNPVersion("microcodeVersion", "Latest") // check uppercase also works
m.setAzureSEVSNPVersion("teeVersion", 2) m.setSEVSNPVersion("teeVersion", 2)
m.setAzureSEVSNPVersion("bootloaderVersion", 1) m.setSEVSNPVersion("bootloaderVersion", 1)
return m return m
}(), }(),
@ -181,7 +182,7 @@ func TestReadConfigFile(t *testing.T) {
config: func() configMap { config: func() configMap {
conf := Default() conf := Default()
m := getConfigAsMap(conf, t) m := getConfigAsMap(conf, t)
m.setAzureSEVSNPVersion("microcodeVersion", "1a") m.setSEVSNPVersion("microcodeVersion", "1a")
return m return m
}(), }(),
configName: constants.ConfigFilename, configName: constants.ConfigFilename,
@ -1053,7 +1054,7 @@ func TestIsAppClientIDError(t *testing.T) {
// configMap is used to un-/marshal the config as an unstructured map. // configMap is used to un-/marshal the config as an unstructured map.
type configMap map[string]interface{} type configMap map[string]interface{}
func (c configMap) setAzureSEVSNPVersion(versionType string, value interface{}) { func (c configMap) setSEVSNPVersion(versionType string, value interface{}) {
c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] = value c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] = value
} }
@ -1061,7 +1062,7 @@ func (c configMap) setAzureProvider(azureProviderField string, value interface{}
c["provider"].(configMap)["azure"].(configMap)[azureProviderField] = value c["provider"].(configMap)["azure"].(configMap)[azureProviderField] = value
} }
func (c configMap) getAzureSEVSNPVersion(versionType string) interface{} { func (c configMap) getSEVSNPVersion(versionType string) interface{} {
return c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] return c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType]
} }
@ -1079,25 +1080,23 @@ func getConfigAsMap(conf *Config, t *testing.T) (res configMap) {
type stubAttestationFetcher struct{} type stubAttestationFetcher struct{}
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionList) (attestationconfigapi.AzureSEVSNPVersionList, error) { func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) {
return attestationconfigapi.AzureSEVSNPVersionList( return attestationconfigapi.SEVSNPVersionList{}, nil
[]string{},
), nil
} }
func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionAPI) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { func (f stubAttestationFetcher) FetchSEVSNPVersion(_ context.Context, _ attestationconfigapi.SEVSNPVersionAPI) (attestationconfigapi.SEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{ return attestationconfigapi.SEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, SEVSNPVersion: testCfg,
}, nil }, nil
} }
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { func (f stubAttestationFetcher) FetchSEVSNPVersionLatest(_ context.Context, _ variant.Variant) (attestationconfigapi.SEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{ return attestationconfigapi.SEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, SEVSNPVersion: testCfg,
}, nil }, nil
} }
var testCfg = attestationconfigapi.AzureSEVSNPVersion{ var testCfg = attestationconfigapi.SEVSNPVersion{
Microcode: 93, Microcode: 93,
TEE: 0, TEE: 0,
SNP: 6, SNP: 6,