cli: fix Azure SEV-SNP latest version logic (#2343)

This commit is contained in:
Adrian Stobbe 2023-09-25 11:53:02 +02:00 committed by GitHub
parent 2776e40df7
commit 118f789c2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 547 additions and 245 deletions

View File

@ -97,5 +97,5 @@ runs:
for file in $(ls maa-claims-*.json); do
path=$(realpath "${file}")
cat "${path}"
bazel run //internal/api/attestationconfigapi -- --maa-claims-path "${path}"
bazel run //internal/api/attestationconfigapi/cli -- --maa-claims-path "${path}"
done

View File

@ -71,7 +71,7 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error {
}
cfm := &configFetchMeasurementsCmd{log: log, canFetchMeasurements: featureset.CanFetchMeasurements}
fetcher := attestationconfigapi.NewFetcherWithClient(http.DefaultClient)
fetcher := attestationconfigapi.NewFetcherWithClient(http.DefaultClient, constants.CDNRepositoryURL)
return cfm.configFetchMeasurements(cmd, sigstore.NewCosignVerifier, rekor, fileHandler, fetcher, http.DefaultClient)
}

View File

@ -15,7 +15,6 @@ import (
"net/url"
"strconv"
"testing"
"time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
@ -307,7 +306,7 @@ func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ att
}, nil
}
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context, _ time.Time) (attestationconfigapi.AzureSEVSNPVersionAPI, error) {
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg,
}, nil

View File

@ -11,7 +11,6 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
@ -176,6 +175,6 @@ func (s *stubConfigFetcher) FetchAzureSEVSNPVersionList(context.Context, attesta
panic("not implemented")
}
func (s *stubConfigFetcher) FetchAzureSEVSNPVersionLatest(context.Context, time.Time) (attestationconfigapi.AzureSEVSNPVersionAPI, error) {
func (s *stubConfigFetcher) FetchAzureSEVSNPVersionLatest(context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{}, s.fetchLatestErr
}

View File

@ -0,0 +1,17 @@
# Attestation config API
## Azure SEV-SNP
The version numbers of SEV-SNP are updated as part of [e2e_verify](/.github/actions/e2e_verify/action.yml).
Because the version numbers are not publicly posted by Azure, we observe the versions on Azure VMs and assume a global rollout after a threshold time.
This estimate might make manual intervention necessary when a global rollout didn't happen.
### Manually delete a version
```
COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY="$(cat $PATH_TO_KEY)" AWS_ACCESS_KEY_ID=$ID AWS_ACCESS_KEY=$KEY bazel run //internal/api/attestationconfigapi/cli delete -- --version 2023-09-02-12-52
```
### Manually upload a version
```
COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY="$(cat $PATH_TO_KEY)" AWS_ACCESS_KEY_ID=$ID AWS_ACCESS_KEY=$KEY bazel run //internal/api/attestationconfigapi/cli -- --force --version 2023-09-02-12-52 --maa-claims-path "${path}"
```

2
go.mod
View File

@ -53,6 +53,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.0.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/aws/aws-sdk-go v1.44.297
github.com/aws/aws-sdk-go-v2 v1.18.1
github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/credentials v1.13.26
@ -162,7 +163,6 @@ require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.44.297 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect

View File

@ -88,6 +88,7 @@ require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.44.297 // indirect
github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect

View File

@ -138,6 +138,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.44.297 h1:uL4EV0gQxotQVYegIoBqK079328MOJqgG95daFYSkAM=
github.com/aws/aws-sdk-go v1.44.297/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
@ -1159,6 +1161,7 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@ -1265,6 +1268,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1274,6 +1278,7 @@ golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

View File

@ -8,6 +8,7 @@ go_library(
"azure.go",
"client.go",
"fetcher.go",
"reporter.go",
],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi",
visibility = ["//:__subpackages__"],
@ -19,6 +20,8 @@ go_library(
"//internal/logger",
"//internal/sigstore",
"//internal/staticupload",
"@com_github_aws_aws_sdk_go//aws",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
],
)
@ -27,7 +30,11 @@ go_test(
srcs = [
"client_test.go",
"fetcher_test.go",
"reporter_test.go",
],
embed = [":attestationconfigapi"],
deps = ["@com_github_stretchr_testify//assert"],
deps = [
"//internal/constants",
"@com_github_stretchr_testify//assert",
],
)

View File

@ -8,13 +8,11 @@ package attestationconfigapi
import (
"fmt"
"net/url"
"path"
"sort"
"strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/constants"
)
// attestationURLPath is the URL path to the attestation versions.
@ -41,11 +39,6 @@ type AzureSEVSNPVersionAPI struct {
AzureSEVSNPVersion
}
// URL returns the URL for the request to the config api.
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 AzureSEVSNPVersionAPI) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), i.Version)
@ -67,11 +60,6 @@ func (i AzureSEVSNPVersionAPI) Validate() error {
// AzureSEVSNPVersionList is the request to list all versions in the config api.
type AzureSEVSNPVersionList []string
// URL returns the URL for the request to the config api.
func (i AzureSEVSNPVersionList) URL() (string, error) {
return getURL(i)
}
// 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")
@ -94,16 +82,3 @@ 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

@ -21,6 +21,9 @@ go_library(
"//internal/constants",
"//internal/logger",
"//internal/staticupload",
"@com_github_aws_aws_sdk_go//aws",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//types",
"@com_github_spf13_cobra//:cobra",
"@org_uber_go_zap//:zap",
],
@ -28,13 +31,9 @@ go_library(
go_test(
name = "cli_test",
srcs = [
"delete_test.go",
"main_test.go",
],
srcs = ["delete_test.go"],
embed = [":cli_lib"],
deps = [
"//internal/api/attestationconfigapi",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],

View File

@ -10,6 +10,9 @@ import (
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go/aws"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
@ -27,6 +30,13 @@ func newDeleteCmd() *cobra.Command {
}
cmd.Flags().StringP("version", "v", "", "Name of the version to delete (without .json suffix)")
must(cmd.MarkFlagRequired("version"))
recursivelyCmd := &cobra.Command{
Use: "recursive",
Short: "delete all objects from the API path",
RunE: runRecursiveDelete,
}
cmd.AddCommand(recursivelyCmd)
return cmd
}
@ -59,17 +69,19 @@ func runDelete(cmd *cobra.Command, _ []string) (retErr error) {
return fmt.Errorf("getting bucket: %w", err)
}
distribution, err := cmd.Flags().GetString("distribution")
testing, err := cmd.Flags().GetBool("testing")
if err != nil {
return fmt.Errorf("getting distribution: %w", err)
return fmt.Errorf("getting testing flag: %w", err)
}
_, distribution := getEnvironment(testing)
cfg := staticupload.Config{
Bucket: bucket,
Region: region,
DistributionID: distribution,
}
client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg, []byte(cosignPwd), []byte(privateKey), false, log)
client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg,
[]byte(cosignPwd), []byte(privateKey), false, 1, log)
if err != nil {
return fmt.Errorf("create attestation client: %w", err)
}
@ -85,3 +97,64 @@ func runDelete(cmd *cobra.Command, _ []string) (retErr error) {
}
return deleteCmd.delete(cmd)
}
func runRecursiveDelete(cmd *cobra.Command, _ []string) (retErr error) {
region, err := cmd.Flags().GetString("region")
if err != nil {
return fmt.Errorf("getting region: %w", err)
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return fmt.Errorf("getting bucket: %w", err)
}
testing, err := cmd.Flags().GetBool("testing")
if err != nil {
return fmt.Errorf("getting testing flag: %w", err)
}
_, distribution := getEnvironment(testing)
log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi")
client, closeFn, err := staticupload.New(cmd.Context(), staticupload.Config{
Bucket: bucket,
Region: region,
DistributionID: distribution,
}, log)
if err != nil {
return fmt.Errorf("create static upload client: %w", err)
}
defer func() {
err := closeFn(cmd.Context())
if err != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to close client: %w", err))
}
}()
path := "constellation/v1/attestation/azure-sev-snp"
resp, err := client.ListObjectsV2(cmd.Context(), &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String(path),
})
if err != nil {
return err
}
// Delete all objects in the path.
objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents))
for i, obj := range resp.Contents {
objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key}
}
if len(objIDs) > 0 {
_, err = client.DeleteObjects(cmd.Context(), &s3.DeleteObjectsInput{
Bucket: aws.String(bucket),
Delete: &s3types.Delete{
Objects: objIDs,
Quiet: true,
},
})
if err != nil {
return err
}
}
return nil
}

View File

@ -17,58 +17,123 @@ fi
configapi_cli=$(realpath @@CONFIGAPI_CLI@@)
stat "${configapi_cli}" >> /dev/null
configapi_cli="${configapi_cli} --testing"
###### script body ######
readonly region="eu-west-1"
readonly bucket="resource-api-testing"
readonly distribution="ETZGUP1CWRC2P"
tmpdir=$(mktemp -d)
readonly tmpdir
registerExitHandler "rm -rf $tmpdir"
# empty the bucket version state
${configapi_cli} delete recursive --region "$region" --bucket "$bucket"
# the high version numbers ensure that it's newer than the current latest value
readonly current_claim_path="$tmpdir/currentMaaClaim.json"
cat << EOF > "$current_claim_path"
{
"x-ms-isolation-tee": {
"x-ms-sevsnpvm-tee-svn": 1,
"x-ms-sevsnpvm-snpfw-svn": 1,
"x-ms-sevsnpvm-microcode-svn": 1,
"x-ms-sevsnpvm-bootloader-svn": 1
}
}
EOF
# upload a fake latest version for the fetcher
${configapi_cli} --force --maa-claims-path "$current_claim_path" --upload-date "2000-01-01-01-01" --region "$region" --bucket "$bucket"
# the high version numbers ensure that it's newer than the current latest value
readonly claim_path="$tmpdir/maaClaim.json"
cat << EOF > "$claim_path"
{
"x-ms-isolation-tee": {
"x-ms-sevsnpvm-tee-svn": 1,
"x-ms-sevsnpvm-snpfw-svn": 9,
"x-ms-sevsnpvm-microcode-svn": 116,
"x-ms-sevsnpvm-bootloader-svn": 4
"x-ms-sevsnpvm-tee-svn": 255,
"x-ms-sevsnpvm-snpfw-svn": 255,
"x-ms-sevsnpvm-microcode-svn": 255,
"x-ms-sevsnpvm-bootloader-svn": 255
}
}
EOF
readonly date="2023-02-02-03-04"
${configapi_cli} --maa-claims-path "$claim_path" --upload-date "$date" --region "$region" --bucket "$bucket" --distribution "$distribution"
# has an older version
readonly older_claim_path="$tmpdir/maaClaimOld.json"
cat << EOF > "$older_claim_path"
{
"x-ms-isolation-tee": {
"x-ms-sevsnpvm-tee-svn": 255,
"x-ms-sevsnpvm-snpfw-svn": 255,
"x-ms-sevsnpvm-microcode-svn": 254,
"x-ms-sevsnpvm-bootloader-svn": 255
}
}
EOF
# report 3 versions with different dates to fill the reporter cache
readonly date_oldest="2023-02-01-03-04"
${configapi_cli} --maa-claims-path "$older_claim_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3
readonly date_older="2023-02-02-03-04"
${configapi_cli} --maa-claims-path "$older_claim_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3
readonly date="2023-02-03-03-04"
${configapi_cli} --maa-claims-path "$claim_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3
# expect that $date_oldest is served as latest version
baseurl="https://d33dzgxuwsgbpw.cloudfront.net/constellation/v1/attestation/azure-sev-snp"
if ! curl -fsSL ${baseurl}/${date}.json > /dev/null; then
echo "Checking for uploaded version file constellation/v1/attestation/azure-sev-snp/${date}.json: request returned ${?}"
if ! curl -fsSL ${baseurl}/${date_oldest}.json > version.json; then
echo "Checking for uploaded version file constellation/v1/attestation/azure-sev-snp/${date_oldest}.json: request returned ${?}"
exit 1
fi
if ! curl -fsSL ${baseurl}/${date}.json.sig > /dev/null; then
echo "Checking for uploaded version signature file constellation/v1/attestation/azure-sev-snp/${date}.json.sig: request returned ${?}"
# check that version values are equal to expected
if ! cmp -s <(echo -n '{"bootloader":255,"tee":255,"snp":255,"microcode":254}') version.json; then
echo "The version content:"
cat version.json
echo " is not equal to the expected version content:"
echo '{"bootloader":255,"tee":255,"snp":255,"microcode":254}'
exit 1
fi
if ! curl -fsSL ${baseurl}/list > /dev/null; then
if ! curl -fsSL ${baseurl}/${date_oldest}.json.sig > /dev/null; then
echo "Checking for uploaded version signature file constellation/v1/attestation/azure-sev-snp/${date_oldest}.json.sig: request returned ${?}"
exit 1
fi
# check list endpoint
if ! curl -fsSL ${baseurl}/list > list.json; then
echo "Checking for uploaded list file constellation/v1/attestation/azure-sev-snp/list: request returned ${?}"
exit 1
fi
${configapi_cli} delete --version "$date" --region "$region" --bucket "$bucket" --distribution "$distribution"
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date}.json)
if [[ $http_code -ne 404 ]]; then
echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date}.json, but got ${http_code}"
# check that version values are equal to expected
if ! cmp -s <(echo -n '["2023-02-01-03-04.json","2000-01-01-01-01.json"]') list.json; then
echo "The list content:"
cat list.json
echo " is not equal to the expected version content:"
echo '["2023-02-01-03-04.json","2000-01-01-01-01.json"]'
exit 1
fi
# check that the other versions are not uploaded
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_older}.json)
if [[ $http_code -ne 404 ]]; then
echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date_older}.json, but got ${http_code}"
exit 1
fi
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date}.json.sig)
if [[ $http_code -ne 404 ]]; then
echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date}.json, but got ${http_code}"
exit 1
fi
${configapi_cli} delete --version "$date_oldest" --region "$region" --bucket "$bucket"
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_oldest}.json)
if [[ $http_code -ne 404 ]]; then
echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date_oldest}.json, but got ${http_code}"
exit 1
fi
# Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail.
http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_oldest}.json.sig)
if [[ $http_code -ne 404 ]]; then
echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date_oldest}.json, but got ${http_code}"
exit 1
fi

View File

@ -8,8 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
This package provides a CLI to interact with the Attestationconfig API, a sub API of the Resource API.
You can execute an e2e test by running: `bazel run //internal/api/attestationconfigapi:configapi_e2e_test`.
The CLI is used in the CI pipeline. Actions that change the bucket's data shouldn't be executed manually.
Notice that there is no synchronization on API operations.
The CLI is used in the CI pipeline. Manual actions that change the bucket's data shouldn't be necessary.
The reporter CLI caches the observed version values in a dedicated caching directory and derives the latest API version from it.
Any version update is then pushed to the API.
*/
package main
@ -35,6 +36,8 @@ const (
distributionID = constants.CDNDefaultDistributionID
envCosignPwd = "COSIGN_PASSWORD"
envCosignPrivateKey = "COSIGN_PRIVATE_KEY"
// versionWindowSize defines the number of versions to be considered for the latest version. Each week 5 versions are uploaded for each node of the verify cluster.
versionWindowSize = 15
)
var (
@ -53,10 +56,10 @@ func main() {
// newRootCmd creates the root command.
func newRootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "COSIGN_PASSWORD=$CPWD COSIGN_PRIVATE_KEY=$CKEY upload --version-file $FILE",
Use: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY upload --version-file $FILE",
Short: "Upload a set of versions specific to the azure-sev-snp attestation variant to the config api.",
Long: fmt.Sprintf("Upload a set of versions specific to the azure-sev-snp attestation variant to the config api."+
Long: fmt.Sprintf("The CLI uploads an observed version number specific to the azure-sev-snp attestation variant to a cache directory. The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+
"Please authenticate with AWS through your preferred method (e.g. environment variables, CLI)"+
"to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.",
envCosignPrivateKey, envCosignPwd,
@ -66,9 +69,12 @@ func newRootCmd() *cobra.Command {
}
rootCmd.Flags().StringP("maa-claims-path", "t", "", "File path to a json file containing the MAA claims.")
rootCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.")
rootCmd.Flags().BoolP("force", "f", false, "Use force to manually push a new latest version."+
" The version gets saved to the cache but the version selection logic is skipped.")
rootCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.")
rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.")
rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.")
rootCmd.PersistentFlags().StringP("distribution", "i", distributionID, "cloudflare distribution used.")
rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.")
must(rootCmd.MarkFlagRequired("maa-claims-path"))
rootCmd.AddCommand(newDeleteCmd())
return rootCmd
@ -110,23 +116,8 @@ func runCmd(cmd *cobra.Command, _ []string) (retErr error) {
inputVersion := maaTCB.ToAzureSEVSNPVersion()
log.Infof("Input version: %+v", inputVersion)
latestAPIVersionAPI, err := attestationconfigapi.NewFetcher().FetchAzureSEVSNPVersionLatest(ctx, flags.uploadDate)
if err != nil {
return fmt.Errorf("fetching latest version: %w", err)
}
latestAPIVersion := latestAPIVersionAPI.AzureSEVSNPVersion
isNewer, err := isInputNewerThanLatestAPI(inputVersion, latestAPIVersion)
if err != nil {
return fmt.Errorf("comparing versions: %w", err)
}
if !isNewer {
log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion)
return nil
}
log.Infof("Input version: %+v is newer than latest API version: %+v", inputVersion, latestAPIVersion)
client, clientClose, err := attestationconfigapi.NewClient(ctx, cfg, []byte(cosignPwd), []byte(privateKey), false, log)
client, clientClose, err := attestationconfigapi.NewClient(ctx, cfg,
[]byte(cosignPwd), []byte(privateKey), false, flags.cacheWindowSize, log)
defer func() {
err := clientClose(cmd.Context())
if err != nil {
@ -138,64 +129,98 @@ func runCmd(cmd *cobra.Command, _ []string) (retErr error) {
return fmt.Errorf("creating client: %w", err)
}
if err := client.UploadAzureSEVSNPVersion(ctx, inputVersion, flags.uploadDate); err != nil {
return fmt.Errorf("uploading version: %w", err)
latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(flags.url, constants.CosignPublicKeyDev).FetchAzureSEVSNPVersionLatest(ctx)
if err != nil {
if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) {
log.Infof("No versions found in API, but assuming that we are uploading the first version.")
} else {
return fmt.Errorf("fetching latest version: %w", err)
}
}
latestAPIVersion := latestAPIVersionAPI.AzureSEVSNPVersion
if err := client.UploadAzureSEVSNPVersionLatest(ctx, inputVersion, latestAPIVersion, flags.uploadDate, flags.force); err != nil {
if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) {
log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion)
return nil
}
return fmt.Errorf("updating latest version: %w", err)
}
cmd.Printf("Successfully uploaded new Azure SEV-SNP version: %+v\n", inputVersion)
return nil
}
type cliFlags struct {
maaFilePath string
uploadDate time.Time
region string
bucket string
distribution string
type config struct {
maaFilePath string
uploadDate time.Time
region string
bucket string
distribution string
url string
force bool
cacheWindowSize int
}
func parseCliFlags(cmd *cobra.Command) (cliFlags, error) {
func parseCliFlags(cmd *cobra.Command) (config, error) {
maaFilePath, err := cmd.Flags().GetString("maa-claims-path")
if err != nil {
return cliFlags{}, fmt.Errorf("getting maa claims path: %w", err)
return config{}, fmt.Errorf("getting maa claims path: %w", err)
}
dateStr, err := cmd.Flags().GetString("upload-date")
if err != nil {
return cliFlags{}, fmt.Errorf("getting upload date: %w", err)
return config{}, fmt.Errorf("getting upload date: %w", err)
}
uploadDate := time.Now()
if dateStr != "" {
uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr)
if err != nil {
return cliFlags{}, fmt.Errorf("parsing date: %w", err)
return config{}, fmt.Errorf("parsing date: %w", err)
}
}
region, err := cmd.Flags().GetString("region")
if err != nil {
return cliFlags{}, fmt.Errorf("getting region: %w", err)
return config{}, fmt.Errorf("getting region: %w", err)
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return cliFlags{}, fmt.Errorf("getting bucket: %w", err)
return config{}, fmt.Errorf("getting bucket: %w", err)
}
distribution, err := cmd.Flags().GetString("distribution")
testing, err := cmd.Flags().GetBool("testing")
if err != nil {
return cliFlags{}, fmt.Errorf("getting distribution: %w", err)
return config{}, fmt.Errorf("getting testing flag: %w", err)
}
url, distribution := getEnvironment(testing)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return config{}, fmt.Errorf("getting force: %w", err)
}
return cliFlags{
maaFilePath: maaFilePath,
uploadDate: uploadDate,
region: region,
bucket: bucket,
distribution: distribution,
cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size")
if err != nil {
return config{}, fmt.Errorf("getting cache window size: %w", err)
}
return config{
maaFilePath: maaFilePath,
uploadDate: uploadDate,
region: region,
bucket: bucket,
url: url,
distribution: distribution,
force: force,
cacheWindowSize: cacheWindowSize,
}, nil
}
func getEnvironment(testing bool) (url string, distributionID string) {
if testing {
return "https://d33dzgxuwsgbpw.cloudfront.net", "ETZGUP1CWRC2P"
}
return constants.CDNRepositoryURL, constants.CDNDefaultDistributionID
}
// maaTokenTCBClaims describes the TCB information in a MAA token.
type maaTokenTCBClaims struct {
IsolationTEE struct {
@ -215,26 +240,6 @@ func (c maaTokenTCBClaims) ToAzureSEVSNPVersion() attestationconfigapi.AzureSEVS
}
}
// isInputNewerThanLatestAPI compares all version fields with the latest API version and returns true if any input field is newer.
func isInputNewerThanLatestAPI(input, latest attestationconfigapi.AzureSEVSNPVersion) (bool, error) {
if input == latest {
return false, nil
}
if input.TEE < latest.TEE {
return false, fmt.Errorf("input TEE version: %d is older than latest API version: %d", input.TEE, latest.TEE)
}
if input.SNP < latest.SNP {
return false, fmt.Errorf("input SNP version: %d is older than latest API version: %d", input.SNP, latest.SNP)
}
if input.Microcode < latest.Microcode {
return false, fmt.Errorf("input Microcode version: %d is older than latest API version: %d", input.Microcode, latest.Microcode)
}
if input.Bootloader < latest.Bootloader {
return false, fmt.Errorf("input Bootloader version: %d is older than latest API version: %d", input.Bootloader, latest.Bootloader)
}
return true, nil
}
func must(err error) {
if err != nil {
panic(err)

View File

@ -7,6 +7,7 @@ package attestationconfigapi
import (
"context"
"errors"
"fmt"
"time"
@ -14,6 +15,7 @@ import (
"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"
)
@ -22,30 +24,32 @@ 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
s3Client *apiclient.Client
s3ClientClose func(ctx context.Context) error
bucketID string
signer sigstore.Signer
cacheWindowSize int
}
// 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) {
func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte, dryRun bool, versionWindowSize int, 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,
s3Client: s3Client,
s3ClientClose: clientClose,
signer: sigstore.NewSigner(cosignPwd, privateKey),
bucketID: cfg.Bucket,
cacheWindowSize: versionWindowSize,
}
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.
func (a Client) UploadAzureSEVSNPVersion(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error {
// 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.
func (a Client) uploadAzureSEVSNPVersion(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)
@ -73,6 +77,10 @@ func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string
if attestation.Equal(variant.AzureSEVSNP{}) {
versions, err := apiclient.Fetch(ctx, a.s3Client, AzureSEVSNPVersionList{})
if err != nil {
var notFoundErr *apiclient.NotFoundError
if errors.As(err, &notFoundErr) {
return nil, nil
}
return nil, err
}
return versions, nil

View File

@ -8,97 +8,93 @@ package attestationconfigapi
import (
"context"
"errors"
"fmt"
"strings"
"time"
apifetcher "github.com/edgelesssys/constellation/v2/internal/api/fetcher"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
)
// minimumAgeVersion is the minimum age to accept the version as latest.
const minimumAgeVersion = 14 * 24 * time.Hour
const cosignPublicKey = constants.CosignPublicKeyReleases
// ErrNoVersionsFound is returned if no versions are found.
var ErrNoVersionsFound = errors.New("no versions found")
// Fetcher fetches config API resources without authentication.
type Fetcher interface {
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error)
FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context, now time.Time) (AzureSEVSNPVersionAPI, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context) (AzureSEVSNPVersionAPI, error)
}
// fetcher fetches AttestationCfg API resources without authentication.
type fetcher struct {
apifetcher.HTTPClient
cdnURL string
verifier sigstore.Verifier
}
// NewFetcher returns a new apifetcher.
func NewFetcher() Fetcher {
return NewFetcherWithClient(apifetcher.NewHTTPClient())
return NewFetcherWithClient(apifetcher.NewHTTPClient(), constants.CDNRepositoryURL)
}
// NewFetcherWithCustomCDNAndCosignKey returns a new fetcher with custom CDN URL.
func NewFetcherWithCustomCDNAndCosignKey(cdnURL, cosignKey string) Fetcher {
verifier, err := sigstore.NewCosignVerifier([]byte(cosignKey))
if err != nil {
// This relies on an embedded public key. If this key can not be validated, there is no way to recover from this.
panic(fmt.Errorf("creating cosign verifier: %w", err))
}
return newFetcherWithClientAndVerifier(apifetcher.NewHTTPClient(), verifier, cdnURL)
}
// NewFetcherWithClient returns a new fetcher with custom http client.
func NewFetcherWithClient(client apifetcher.HTTPClient) Fetcher {
func NewFetcherWithClient(client apifetcher.HTTPClient, cdnURL string) Fetcher {
verifier, err := sigstore.NewCosignVerifier([]byte(cosignPublicKey))
if err != nil {
// This relies on an embedded public key. If this key can not be validated, there is no way to recover from this.
panic(fmt.Errorf("creating cosign verifier: %w", err))
}
return newFetcherWithClientAndVerifier(client, verifier)
return newFetcherWithClientAndVerifier(client, verifier, cdnURL)
}
func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifier sigstore.Verifier) Fetcher {
return &fetcher{client, cosignVerifier}
func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifier sigstore.Verifier, url string) Fetcher {
return &fetcher{HTTPClient: client, verifier: cosignVerifier, cdnURL: url}
}
// FetchAzureSEVSNPVersionList fetches the version list information from the config API.
func (f *fetcher) FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, error) {
// TODO(derpsteb): Replace with FetchAndVerify once we move to v2 of the config API.
return apifetcher.Fetch(ctx, f.HTTPClient, attestation)
return apifetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, attestation)
}
// FetchAzureSEVSNPVersion fetches the version information from the config API.
func (f *fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error) {
fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, azureVersion, f.verifier)
fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, azureVersion, f.verifier)
if err != nil {
return fetchedVersion, fmt.Errorf("fetch version %s: %w", fetchedVersion.Version, err)
return fetchedVersion, fmt.Errorf("fetching version %s: %w", azureVersion.Version, err)
}
return fetchedVersion, nil
}
// FetchAzureSEVSNPVersionLatest returns the latest versions of the given type.
func (f *fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context, now time.Time) (res AzureSEVSNPVersionAPI, err error) {
func (f *fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res AzureSEVSNPVersionAPI, err error) {
var list AzureSEVSNPVersionList
list, err = f.FetchAzureSEVSNPVersionList(ctx, list)
if err != nil {
return res, fmt.Errorf("fetching versions list: %w", err)
return res, ErrNoVersionsFound
}
getVersionRequest, err := getLatestVersionOlderThanMinimumAge(list, now, minimumAgeVersion)
if err != nil {
return res, fmt.Errorf("finding latest valid version: %w", err)
if len(list) < 1 {
return res, ErrNoVersionsFound
}
getVersionRequest := AzureSEVSNPVersionAPI{
Version: list[0], // latest version is first in list
}
res, err = f.FetchAzureSEVSNPVersion(ctx, getVersionRequest)
if err != nil {
return res, fmt.Errorf("fetching version: %w", err)
return res, err
}
return
}
func getLatestVersionOlderThanMinimumAge(list AzureSEVSNPVersionList, now time.Time, minimumAgeVersion time.Duration) (AzureSEVSNPVersionAPI, error) {
SortAzureSEVSNPVersionList(list)
for _, v := range list {
dateStr := strings.TrimSuffix(v, ".json")
versionDate, err := time.Parse(VersionFormat, dateStr)
if err != nil {
return AzureSEVSNPVersionAPI{}, fmt.Errorf("parsing version date %s: %w", dateStr, err)
}
if now.Sub(versionDate) > minimumAgeVersion {
return AzureSEVSNPVersionAPI{Version: v}, nil
}
}
return AzureSEVSNPVersionAPI{}, fmt.Errorf("no valid version fulfilling minimum age found")
}

View File

@ -16,11 +16,11 @@ import (
"testing"
"time"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/stretchr/testify/assert"
)
func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
now := time.Date(2023, 6, 12, 0, 0, 0, 0, time.UTC)
latestStr := "2023-06-11-14-09.json"
olderStr := "2019-01-01-01-01.json"
testcases := map[string]struct {
@ -29,21 +29,10 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
wantErr bool
want AzureSEVSNPVersionAPI
}{
"get latest version if older than 2 weeks": {
"get latest version": {
fetcherVersions: []string{latestStr, olderStr},
timeAtTest: now.Add(days(15)),
want: latestVersion,
},
"get older version if latest version is not older than minimum age": {
fetcherVersions: []string{"2023-06-11-14-09.json", "2019-01-01-01-01.json"},
timeAtTest: now.Add(days(7)),
want: olderVersion,
},
"fail when no version is older minimum age": {
fetcherVersions: []string{"2021-02-21-01-01.json", "2021-02-20-00-00.json"},
timeAtTest: now.Add(days(2)),
wantErr: true,
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
@ -54,8 +43,8 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
olderVersion: olderStr,
},
}
fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{})
res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background(), tc.timeAtTest)
fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{}, constants.CDNRepositoryURL)
res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background())
assert := assert.New(t)
if tc.wantErr {
assert.Error(err)
@ -85,10 +74,6 @@ var olderVersion = AzureSEVSNPVersionAPI{
},
}
func days(days int) time.Duration {
return time.Duration(days*24) * time.Hour
}
type fakeConfigAPIHandler struct {
versions []string
latestVersion string

View File

@ -0,0 +1,182 @@
/*
Copyright (c) Edgeless Systems GmbH
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.
*/
package attestationconfigapi
import (
"context"
"errors"
"fmt"
"path"
"sort"
"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/client"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
)
// cachedVersionsSubDir is the subdirectory in the bucket where the cached versions are stored.
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.
var ErrNoNewerVersion = errors.New("input version is not newer than latest API version")
// UploadAzureSEVSNPVersionLatest 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) UploadAzureSEVSNPVersionLatest(ctx context.Context, inputVersion,
latestAPIVersion AzureSEVSNPVersion, now time.Time, force bool,
) error {
if err := c.cacheAzureSEVSNPVersion(ctx, inputVersion, now); err != nil {
return fmt.Errorf("reporting version: %w", err)
}
if force {
return c.uploadAzureSEVSNPVersion(ctx, inputVersion, now)
}
versionDates, err := c.listCachedVersions(ctx)
if err != nil {
return fmt.Errorf("list reported versions: %w", err)
}
if len(versionDates) < c.cacheWindowSize {
c.s3Client.Logger.Warnf("Skipping version update, found %d, expected %d reported versions.", len(versionDates), c.cacheWindowSize)
return nil
}
minVersion, minDate, err := c.findMinVersion(ctx, versionDates)
if err != nil {
return fmt.Errorf("get minimal version: %w", err)
}
c.s3Client.Logger.Infof("Found minimal version: %+v with date: %s", minVersion, minDate)
shouldUpdateAPI, err := isInputNewerThanOtherVersion(minVersion, latestAPIVersion)
if err != nil {
return ErrNoNewerVersion
}
if !shouldUpdateAPI {
c.s3Client.Logger.Infof("Input version: %+v is not newer than latest API version: %+v", minVersion, latestAPIVersion)
return nil
}
c.s3Client.Logger.Infof("Input version: %+v is newer than latest API version: %+v", minVersion, latestAPIVersion)
t, err := time.Parse(VersionFormat, minDate)
if err != nil {
return fmt.Errorf("parsing date: %w", err)
}
if err := c.uploadAzureSEVSNPVersion(ctx, minVersion, t); err != nil {
return fmt.Errorf("uploading version: %w", err)
}
c.s3Client.Logger.Infof("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion)
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.
func (c Client) cacheAzureSEVSNPVersion(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error {
dateStr := date.Format(VersionFormat) + ".json"
res := putCmd{
apiObject: reportedAzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: version},
signer: c.signer,
}
return res.Execute(ctx, c.s3Client)
}
func (c Client) listCachedVersions(ctx context.Context) ([]string, error) {
list, err := c.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(c.bucketID),
Prefix: aws.String(reportVersionDir),
})
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 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) {
var minimalVersion *AzureSEVSNPVersion
var minimalDate string
sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions
versionDates = versionDates[:c.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, reportedAzureSEVSNPVersionAPI{Version: date + ".json"})
if err != nil {
return AzureSEVSNPVersion{}, "", fmt.Errorf("get object: %w", err)
}
if minimalVersion == nil {
minimalVersion = &obj.AzureSEVSNPVersion
minimalDate = date
} else {
shouldUpdateMinimal, err := isInputNewerThanOtherVersion(*minimalVersion, obj.AzureSEVSNPVersion)
if err != nil {
continue
}
if shouldUpdateMinimal {
minimalVersion = &obj.AzureSEVSNPVersion
minimalDate = date
}
}
}
return *minimalVersion, minimalDate, nil
}
// isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer.
func isInputNewerThanOtherVersion(input, other AzureSEVSNPVersion) (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
}
// reportedAzureSEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
type reportedAzureSEVSNPVersionAPI struct {
Version string `json:"-"`
AzureSEVSNPVersion
}
// JSONPath returns the path to the JSON file for the request to the config api.
func (i reportedAzureSEVSNPVersionAPI) JSONPath() string {
return path.Join(reportVersionDir, i.Version)
}
// ValidateRequest validates the request.
func (i reportedAzureSEVSNPVersionAPI) 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 reportedAzureSEVSNPVersionAPI) Validate() error {
return nil
}

View File

@ -1,21 +1,18 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
package attestationconfigapi
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/stretchr/testify/assert"
)
func TestIsInputNewerThanLatestAPI(t *testing.T) {
newTestCfg := func() attestationconfigapi.AzureSEVSNPVersion {
return attestationconfigapi.AzureSEVSNPVersion{
newTestCfg := func() AzureSEVSNPVersion {
return AzureSEVSNPVersion{
Microcode: 93,
TEE: 0,
SNP: 6,
@ -24,13 +21,13 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
}
testCases := map[string]struct {
latest attestationconfigapi.AzureSEVSNPVersion
input attestationconfigapi.AzureSEVSNPVersion
latest AzureSEVSNPVersion
input AzureSEVSNPVersion
expect bool
errMsg string
}{
"input is older than latest": {
input: func(c attestationconfigapi.AzureSEVSNPVersion) attestationconfigapi.AzureSEVSNPVersion {
input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion {
c.Microcode--
return c
}(newTestCfg()),
@ -39,7 +36,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
errMsg: "input Microcode version: 92 is older than latest API version: 93",
},
"input has greater and smaller version field than latest": {
input: func(c attestationconfigapi.AzureSEVSNPVersion) attestationconfigapi.AzureSEVSNPVersion {
input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion {
c.Microcode++
c.Bootloader--
return c
@ -49,7 +46,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
errMsg: "input Bootloader version: 1 is older than latest API version: 2",
},
"input is newer than latest": {
input: func(c attestationconfigapi.AzureSEVSNPVersion) attestationconfigapi.AzureSEVSNPVersion {
input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion {
c.TEE++
return c
}(newTestCfg()),
@ -64,7 +61,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
isNewer, err := isInputNewerThanLatestAPI(tc.input, tc.latest)
isNewer, err := isInputNewerThanOtherVersion(tc.input, tc.latest)
assert := assert.New(t)
if tc.errMsg != "" {
assert.EqualError(err, tc.errMsg)

View File

@ -5,8 +5,5 @@ go_library(
srcs = ["fetcher.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/fetcher",
visibility = ["//:__subpackages__"],
deps = [
"//internal/constants",
"//internal/sigstore",
],
deps = ["//internal/sigstore"],
)

View File

@ -23,9 +23,9 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
)
@ -36,15 +36,17 @@ func NewHTTPClient() HTTPClient {
// Fetch fetches the given apiObject from the public Constellation CDN.
// Fetch does not require authentication.
func Fetch[T apiObject](ctx context.Context, c HTTPClient, obj T) (T, error) {
func Fetch[T apiObject](ctx context.Context, c HTTPClient, cdnURL string, obj T) (T, error) {
if err := obj.ValidateRequest(); err != nil {
return *new(T), fmt.Errorf("validating request for %T: %w", obj, err)
}
url, err := obj.URL()
urlObj, err := url.Parse(cdnURL)
if err != nil {
return *new(T), fmt.Errorf("getting URL for %T: %w", obj, err)
return *new(T), fmt.Errorf("parsing CDN root URL: %w", err)
}
urlObj.Path = obj.JSONPath()
url := urlObj.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
@ -80,8 +82,8 @@ func Fetch[T apiObject](ctx context.Context, c HTTPClient, obj T) (T, error) {
// The public key used to verify the signature is embedded in the verifier argument.
// FetchAndVerify uses a generic to return a new object of type T.
// Otherwise the caller would have to cast the interface type to a concrete object, which could fail.
func FetchAndVerify[T apiObject](ctx context.Context, c HTTPClient, obj T, cosignVerifier sigstore.Verifier) (T, error) {
fetchedObj, err := Fetch(ctx, c, obj)
func FetchAndVerify[T apiObject](ctx context.Context, c HTTPClient, cdnURL string, obj T, cosignVerifier sigstore.Verifier) (T, error) {
fetchedObj, err := Fetch(ctx, c, cdnURL, obj)
if err != nil {
return fetchedObj, fmt.Errorf("fetching object: %w", err)
}
@ -89,16 +91,10 @@ func FetchAndVerify[T apiObject](ctx context.Context, c HTTPClient, obj T, cosig
if err != nil {
return fetchedObj, fmt.Errorf("marshalling object: %w", err)
}
url, err := obj.URL()
if err != nil {
return fetchedObj, fmt.Errorf("getting signed URL: %w", err)
}
signature, err := Fetch(ctx, c, signature{Signed: url})
signature, err := Fetch(ctx, c, cdnURL, signature{Signed: obj.JSONPath()})
if err != nil {
return fetchedObj, fmt.Errorf("fetching signature: %w", err)
}
err = cosignVerifier.VerifySignature(marshalledObj, signature.Signature)
if err != nil {
return fetchedObj, fmt.Errorf("verifying signature: %w", err)
@ -127,28 +123,24 @@ type HTTPClient interface {
type apiObject interface {
ValidateRequest() error
Validate() error
URL() (string, error)
JSONPath() string
}
// signature manages the signature of a object saved at location 'Signed'.
type signature struct {
// Signed is the object that is signed.
Signed string `json:"signed"`
Signed string `json:"-"`
// Signature is the signature of `Signed`.
Signature []byte `json:"signature"`
}
// URL returns the URL for the request to the config api.
func (s signature) URL() (string, error) {
return s.Signed + ".sig", nil
func (s signature) JSONPath() string {
return s.Signed + ".sig"
}
// ValidateRequest validates the request.
func (s signature) ValidateRequest() error {
if !strings.HasPrefix(s.Signed, constants.CDNRepositoryURL) {
return errors.New("signed object missing CDN URL prefix")
}
if !strings.HasSuffix(s.Signed, ".json") {
return errors.New("signed object missing .json suffix")
}

View File

@ -10,34 +10,36 @@ import (
"context"
"github.com/edgelesssys/constellation/v2/internal/api/fetcher"
"github.com/edgelesssys/constellation/v2/internal/constants"
)
// Fetcher fetches version API resources without authentication.
type Fetcher struct {
fetcher.HTTPClient
cdnURL string
}
// NewFetcher returns a new Fetcher.
func NewFetcher() *Fetcher {
return &Fetcher{fetcher.NewHTTPClient()}
return &Fetcher{fetcher.NewHTTPClient(), constants.CDNRepositoryURL}
}
// FetchVersionList fetches the given version list from the versions API.
func (f *Fetcher) FetchVersionList(ctx context.Context, list List) (List, error) {
return fetcher.Fetch(ctx, f.HTTPClient, list)
return fetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, list)
}
// FetchVersionLatest fetches the latest version from the versions API.
func (f *Fetcher) FetchVersionLatest(ctx context.Context, latest Latest) (Latest, error) {
return fetcher.Fetch(ctx, f.HTTPClient, latest)
return fetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, latest)
}
// FetchImageInfo fetches the given image info from the versions API.
func (f *Fetcher) FetchImageInfo(ctx context.Context, imageInfo ImageInfo) (ImageInfo, error) {
return fetcher.Fetch(ctx, f.HTTPClient, imageInfo)
return fetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, imageInfo)
}
// FetchCLIInfo fetches the given cli info from the versions API.
func (f *Fetcher) FetchCLIInfo(ctx context.Context, cliInfo CLIInfo) (CLIInfo, error) {
return fetcher.Fetch(ctx, f.HTTPClient, cliInfo)
return fetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, cliInfo)
}

View File

@ -14,6 +14,7 @@ import (
"net/http"
"testing"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
@ -189,7 +190,7 @@ func TestFetchVersionList(t *testing.T) {
return tc.serverResp
})
fetcher := Fetcher{client}
fetcher := Fetcher{client, constants.CDNRepositoryURL}
list, err := fetcher.FetchVersionList(context.Background(), tc.list)

View File

@ -9,7 +9,6 @@ import (
"bytes"
"context"
"fmt"
"time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
@ -70,8 +69,8 @@ func (c AzureSEVSNP) EqualTo(old AttestationCfg) (bool, error) {
}
// FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them.
func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher, now time.Time) error {
versions, err := fetcher.FetchAzureSEVSNPVersionLatest(ctx, now)
func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error {
versions, err := fetcher.FetchAzureSEVSNPVersionLatest(ctx)
if err != nil {
return err
}

View File

@ -26,7 +26,6 @@ import (
"os"
"reflect"
"strings"
"time"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
@ -456,7 +455,7 @@ func New(fileHandler file.Handler, name string, fetcher attestationconfigapi.Fet
}
if azure := c.Attestation.AzureSEVSNP; azure != nil {
if err := azure.FetchAndSetLatestVersionNumbers(context.Background(), fetcher, time.Now()); err != nil {
if err := azure.FetchAndSetLatestVersionNumbers(context.Background(), fetcher); err != nil {
return c, err
}
}

View File

@ -11,7 +11,6 @@ import (
"errors"
"reflect"
"testing"
"time"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
@ -1093,7 +1092,7 @@ func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ att
}, nil
}
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context, _ time.Time) (attestationconfigapi.AzureSEVSNPVersionAPI, error) {
func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) {
return attestationconfigapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg,
}, nil