mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-28 17:09:30 -05:00
cli: fix Azure SEV-SNP latest version logic (#2343)
This commit is contained in:
parent
2776e40df7
commit
118f789c2f
2
.github/actions/e2e_verify/action.yml
vendored
2
.github/actions/e2e_verify/action.yml
vendored
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
17
dev-docs/workflows/attestationconfigapi.md
Normal file
17
dev-docs/workflows/attestationconfigapi.md
Normal 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
2
go.mod
@ -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
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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, ¬FoundErr) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return versions, nil
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
182
internal/api/attestationconfigapi/reporter.go
Normal file
182
internal/api/attestationconfigapi/reporter.go
Normal 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
|
||||
}
|
@ -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)
|
@ -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"],
|
||||
)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user